Chapter 8

Durk Jan de Bruin

You Forgot What You Ate

The user of the You Are What You Eat program has more trouble recalling what he ate than anticipated. He also wonders if the computer can keep track of the fat and calories in each thing he eats so that he only has to enter it once. This case study describes how a programmer might respond to problems encountered by users. What does "user-friendly" really mean?


Problem Statement

We encounter Terry a few days after giving him the You Are What You Eat program.

MIKE AND MARCIA: How's the program working out?

TERRY: It's OK, but ... (pause)

MIKE AND MARCIA: We wouldn't mind trying to improve the program a bit, if you have some things to suggest.

TERRY: Well, I always have to look up the amounts of fat and calories each food I eat has. Can you make the program do that instead?

MIKE AND MARCIA: That would be possible. You would want to enter the food and how many servings of it you had, right. Then the program would report how much fat and how many calories you had eaten?

TERRY: Right.

MIKE AND MARCIA: All the information for each food would be stored in a disk file. There will be a lot of it. Do you have room on your disk?

TERRY: I think so.

MIKE AND MARCIA: Are you having any other problems with the program?

TERRY: It would be nice to be able to print a copy of the graph the program produces.

MIKE AND MARCIA: That's possible too. You can get printed versions of text files on your computer, right? What else?

TERRY: On Friday, after running the program, I remembered something else I ate. Then I was gone all day on Saturday, and I was too tired to run the program when I got home. When I ran the program on Sunday I put in Sunday's foods before Saturday's, so they were out of order. To fix the problem, I edited the history file with my text editor even though you told me not to. I made a mistake, so when I ran the program it crashed. I knew it was my fault so I fixed the file, but I was worried that I had messed up something somehow.

MIKE AND MARCIA: Calm down, it's OK, no harm done. Suppose we write a program that will let you change what's in the file. It can give you a chance to correct any mistakes you make and add stuff you forget.

TERRY: That would be great!

MIKE AND MARCIA: OK, give us another day or two.

Analysis

8.1 What additional questions might the programmers have asked Terry in order to clarify the planned modifications?

Analysis

8.2 What additional features might help the user of the program?

Analysis

8.3 What should the program do about foods that are not found in the food information file?

Analysis

8.4 Sketch an interaction between Terry and the revised program.

Reflection

8.5 What are the drawbacks to this process of trial and refinement of the program?

Reflection

8.6 What kinds of programs benefit from the process of trial and refinement used to develop this program?

  1. 1
  2. 2

Chapter 8

Durk Jan de Bruin

Preparation

Solutions in this case study use files, along with arrays and dictionary. This case study introduces the use of enumerated type.


Planning the Modifications

What new features were requested?

Terry requested three new features to make the program more useful:

  • Rather than requiring the user to enter fat and calories for each food eaten, the program should ask for the name of the food and the number of servings eaten, and look up the food in a file. From the information stored in the file, the program should compute the fat and calorie amounts for the food.
  • Rather than just showing the graphs of fat and calories use on the screen, the program should also create a file that can be printed.
  • Rather than assuming that the input from the history file is correctly organized, the program should allow the user to correct errors and insert data into the history file.

Stop & Help

Why didn't Terry ask for these features in the first place?

To determine how to add these features to the program, we review the program structure, expressed in the call diagram below.

Stop & Predict

What parts of these changes require only recycling prior solutions?

What changes will produce a file containing the graphs?

Printing the graphs to a file involves just recycling the solution for printing to the screen. The PrintGraph function is the obvious place to add code. The change involves adding a write statement to initialize the file, and duplicating each print statement to write to the file as well as to the screen. A good name for the file is graphFile. A new question for Terry is whether he has a file by this name. Once the files are created, Terry can print them using whatever functions he normally uses to print text files.

Questions for Terry

  • Do you (or will you ever) have a file named graphFile?

Stop & Help

Why do we need to ask Terry if he has a file named graphFile?

Stop & Help

How do you get printed versions of text files created by programs on your computer system?

What changes will implement the new input format?

The main change to the input is entering a food name and serving size rather than entering fat and calories. The code in You Are What You Eat can be almost completely recycled.

The input functions all have names starting with "read". ReadEntry calls ReadFat and ReadCalories to get values for fat and calories for a food; each of those functions calls ReadTrimmedLine to read a line from the user.

The program should still read lines from the user, so ReadTrimmedLine should work for the new input. ReadTrimmedLine can be used in the revised program to read a line containing "ice cream" or "broccoli." ReadFood and ReadServings are obvious replacements for ReadFat and ReadCalories.

After reading a food name, the program should search the food information file to determine the values to return to ReadEntry. We'll assume that the food information file is called foodinfo; this name must be checked with Terry.

Stop & Predict

What details are being postponed here?

Stop & Help

What changes to the error-checking functions are needed?

  1. 3
  2. 4

Chapter 8

Durk Jan de Bruin

What changes will allow the user to add forgotten data and correct errors?

To change or correct data, Terry needs to be able to edit the history file. Currently the program allows the user only to add the current day's data to the history file. Providing an editing feature means providing a way to add data for other dates to the file and to select data already in the file in order to update it. Updates will have to be reflected in the graphs produced by the program. The current program accesses only the most recent 30 days' data, but Terry might decide to change data gathered much earlier. This feature changes the way Terry will interact with the program and requires more than recycling the prior solution.

Stop & Predict

Should the file editing function be added to the existing program or addressed in a new program?

How might the modifications be incorporated?

We consider the alternatives of adding the editing feature to the current program or creating a new program to edit the history file. Either approach has advantages. Having a separate program and thereby creating a system of programs - results in two smaller programs rather than one big program. It requires (at least in Standard Python) that copies of common code occur in each program. If, for instance, the format of the history file were changed, the new format would have to be implemented in two places rather than one.

It's not clear which option is better for Terry. He might not be able to predict whether it would be easier to use two programs or one program; he would probably want to know more about how the programs would work.

We will create a separate program, which we'll call the history file editor, to update the history file. Creating two programs helps to communicate that each program performs a separate function and is, therefore, more intuitive for the user. The You Are What You Eat program will continue to handle the normal situation in which the current day's data is collected and the history displayed in graphs. The history file editor will be available for the exceptional situation of changing previously entered data.

We can postpone the implementation of the history file editor until we finish updating You Are What You Eat. However, providing the editing facility may have implications for You Are What You Eat, so we keep the editing program in mind while making the modifications.

Stop & Help

What criteria would Terry use to decide about one or two programs? How are these different from the criteria used by a programmer?

Stop & Consider

Compare the approach we're about to take with the approach of having a single program do all the data collection and updating, and another program produce the graphs and statistics.

How will the history file editor work?

The history file editor will repeatedly read and execute commands from the user. The commands will be such things as "list history entries","change this entry," and "add an entry."

How will the history file format change?

Terry seems to think of entries by date. To make the interface intuitive, the entries should be identified by date. For example, the "change" command should be "change the entry for a given date."

In designing You Are What You Eat we decided not to store the date because it was redundant with the sequence of entries. Now there is a need for the date because an entry might be missing or incomplete. Thus, to implement the file editor we need to add the date to each file entry. This means changing You Are What You Eat to include the date with each entry.

How will the date for the current entry be determined?

Figuring out the date for the file entry about to be added can be done in several ways.

  • The You Are What You Eat program can figure it out from the information stored in the history file.
  • The program can ask the user.
  • Some Python environments provide a function to retrieve the current date from the operating system. (Of course, this could cause problems if the data being input is not for the current date.)

Terry intends to dictionary information for every day. Thus the most intuitive approach is to verify that the information to be dictionaryed is for the "next" day. The program should determine the date from the history file by computing the day after the date of the most recent file entry. The program should either print the date or ask the user to verify the date. We add this choice to the list we have for Terry.

Stop & Help

Why is this a question that Terry can answer while choosing between one and two programs was not?

Questions for Terry

  • Do you (or will you ever) have a file named graphFile?
  • Do you (or will you ever) have a file named foodInfo?
  • Is it OK for the program to display the date it will store for the current entry and have you verify it?
  1. 5
  2. 6

Chapter 8

Durk Jan de Bruin

What modifications are needed to go with the new file format?

In designing You Are What You Eat we isolated references to the data structures so that it would be easy to update them without making major changes in the program. The Initialize function reads from historyFile, and the Update function writes to the file. These are the two main places where changes are needed.

Stop & Predict

What other parts of the program are likely to need changes to accommodate the updated file format?

What changes to You Are What You Eat are to be made?

Planned changes to You Are What You Eat are summarized as follows:

  • ReadEntry will be rewritten to read from the user the name and number of servings of a food and to look up the corresponding fat and calorie amounts in foodinfo, the file of food information. Functions ReadFat and ReadCalories will be replaced by functions ReadFood and ReadServings.
  • PrintGraph will be revised to print the graphs to a file as well as to the terminal screen.
  • Initialize will be modified to include a date as part of each file entry read from historyFile, to define a date for the current day's file entry, and to inform the user of the date.
  • Update will be modified to include a date as part of each file entry written to tempFile and historyFile.
  • A date will be included in EntryType.

Stop & Predict

How do these changes influence the decomposition of You Are What You Eat?

Stop & Predict

Which changes are minor and which are major?

What comes first?

A beneficial aspect of the way we designed the program is that the rewritten version of ReadEntry can be tested and debugged independent of the other revisions. The same holds for the change to PrintGraph. We will thus make these revisions before implementing the date-handling code, which seems to require more changes to the program.

Analysis

8.7 Suppose that instead of locating a data file on fat and calories Terry wanted to store the information he entered and add new information only for new foods. How would this change the way the program was updated?

Reflection

8.8 How might the need for the date in the history file have been anticipated in the first version of You Are What You Eat?


Implementing Changes to You Are What You Eat

How is PrintGraph changed?

We start with the easiest change, updating PrintGraph. The only change needed is to add a write statement and to duplicate all the print statements as follows.

print( ... )

is duplicated as

write( ... )
filepointer.write( ... )

Stop & Help

Test the code using the test data designed in You Are What You Eat.

How is ReadEntry rewritten?

The old version of ReadEntry called ReadFat and ReadCalories to read fat and calorie values from the user. It makes sense to retain this structure by calling ReadFood first, then ReadServings. At some point the specified food name must be looked up in foodinfo and associated with fat and calorie figures. These would then be accumulated in the same way as in the original program. Here is a pseudocode design:

entry["fat"] = 0
entry["calories"] = 0
while not done:
ReadFood(inFile, foodEntry, done)
numServings = ReadServings (inFile,
foodEntry, numServings)
entry["fat"] = entry["fat"] +
numServings * fat from food
entry["calories"] = entry["calories"] +
numServings * calories from food

In which function should the food be looked up?

Lookup of the food name entered by the user then must occur in ReadEntry, ReadFood, or ReadServings.

Stop & Predict

Which function should contain the lookup code?

ReadServings would be a poor choice. The number of servings is not related to the kind of food. ReadEntry is a possibility; immediately prior to the call to ReadServings, we might insert code to search the food file.

The best choice, however, is ReadFood. Recall that ReadFat and ReadCalories were designed to return legal values for fat and calories to ReadEntry in the original program. Thus ReadFood should similarly return a legal food to ReadEntry. A food that isn't in the food file is illegal.

  1. 7
  2. 8

Chapter 8

Durk Jan de Bruin

Stop & Help

What kinds of errors should be detected for the food name?

What is the format of the food information file?

As long as ReadFood is looking up the food name, it might as well return the entire entry from foodinfo. Thus foodinfo should be structured to make this easy. The food name, something about a serving size, and the corresponding fat and calorie values should be stored together in foodinfo so that once the name is found, all the other information for the food will be immediately available. One way to represent foodinfo would then be as lines of text, each containing information for one food, for instance:

hamburger 12 125

The spaces are separators for the various pieces of food information. The problem with this format is extracting the components from the line of text.

A better approach is to impose more structure on the file by making it a file of dictionary. Reading a dictionary would involve reading each of the components. Though such a file could not be printed or edited with a text editor, that's fine; the user need not meddle with the file except through this program. The file can be defined as

StringType = ''
FoodEntryType = {
"foodName": '',
"fat": 0,
"calories": 0
}
FoodInfoType = FoodEntryType.copy()

How is a file of dictionary created?

A program is needed to create a file of dictionary - a text editor cannot do it. The program is straightforward, however. All that is needed is a loop containing some input statements and a statement that writes the input values to the file.

How is the error checking coded?

The input and error-checking code that uses this file would then be patterned on code in ReadFat as follows. It uses a Search function that returns either a food entry or an indication that none was found in foodinfo.

found = False
error = False
line = LineType.copy()
done = False
while not done:
error = False
print('Please type a food name')
input1 = input()
if Empty(input1):
error = True
else:
found = Search(input1, foodEntry, found)
if not found:
error = True
if found:
break
if error:
print('That food is not in
the dictionary of foods.')

On exit from the loop, either the user has typed "done" and therefore done is true, or the user has typed a recognized food name, foodEntry contains the information for that food, and done and error are both false.

Stop & Help

Is the prompt sufficient to tell the user that input like "slice of cake" is probably inappropriate? Why or why not?

How is the file search coded?

The Search function searches the foodinfo file for an entry matching the given name. It is similar to linear search in an array:

def Search(line, foodEntry, found):
found = False
foodinfo = open('foodinfo.txt', 'r')
for line1 in foodinfo:
line2 = line1.split()
found = Equal(line2[0], line)
if found == True:
foodEntry ["foodName"] = line2[0]
foodEntry ["fat"] = int(line2[1])
foodEntry ["calories"] = int(line2[2])
break
return found

How are the fat and calorie amounts coded?

The number of servings is read in the same way, using ReadTrimmedLine, IsAllDigits, and IntegerValue, as were the fat and calorie amounts in the original program. For a good user interface, the prompting message should indicate what a serving is, so the program should write the servingDescr field in foodEntry. Once converted to an integer, the input value should be checked for reasonableness: it should be at least 1 and probably less than 100 or so. Here's the code.

error = False
line = LineType.copy()
done = False
while not done:
error = False
print('How many servings? One serving = {}
calories'.format (foodEntry["calories"]))
input1 = input()
if Empty(input1):
error = True
elif not input1.isnumeric():
error = True
else:
numServings = int(input1)
if IsInServingRange (numServings):
break
if not IsInServingRange (numServings):
error = True
if error:
print('You must provide an integer number
of servings no more than', MAXSERVINGS)

  1. 9
  2. 10

Chapter 8

Durk Jan de Bruin

Back in ReadEntry, the number of servings should be multiplied by foodEntry["fat"] to compute a fat amount and by foodEntry["calories"] to compute a calorie amount. Both amounts should be accumulated until the user is finished entering input.

How is code tested?

The revised Read... routines appear in the Python Code section. We test them as follows. First we write a program to create foodInfo; a file containing four elements should be sufficient to check boundary cases. Then we write a main program that repeatedly calls ReadEntry and prints the resulting fat and calorie figures. The only really new code is Search, so it must be tested most carefully; we make sure to check for foods at the start, at the end, and somewhere in the middle of foodInfo.

Stop & Help

Write the program to create foodInfo.

Stop & Help

Write the main program to test ReadEntry.

What modifications are necessary to keep track of dates?

Including dates in the history file requires more widespread change to the program than was true for the other updates.

A date consists of a month, a day, and a year. A good structure for storing a date is a Python dictionary with three fields. In Python, we may then make the following definitions:

MonthType = ("JANUARY", "FEBRUARY", "MARCH", "APRIL", "MAY", "JUNE", "JULY", "AUGUST", "SEPTEMBER", "OCTOBER", "NOVEMBER", "DECEMBER")
DayType = range(1,32)
YearType = range(1990,2100)
DateType = {
"month": '',
"day": 0,
"year": 0
}
EntryType = {
"date": DateType.copy(),
"fat": 0,
"calories": 0
}
HistoryType = []
for i in range(0,30):
HistoryType.append (EntryType.copy())

For clarity, we use range function to represent days and years. We also use Python's enumerated type facility to represent months. In The Calendar Shop, integer constants were used for this purpose. The main difference here is that an enumerated type can be defined somewhat more concisely.

Stop & Predict

What are the advantages of making the history file a file of dictionary?

In the first version of the program, the history file was of type text. It makes sense to have it be a file of dictionary like the food information file, since it will always be read and updated under control of one of the programs we write. Thus, one more definition:

HistoryType = []
for i in range(0,30):
HistoryType.append (EntryType.copy())

Stop & Help

Write a program to create a version of historyFile to use for testing the updated program.

We will have to get together with Terry to copy the fat and calorie data collected so far to a file in the new format.

How will Initialize and Update be modified to handle the new file format?

In You Are What You Eat, we considered and rejected the option of initializing the file to contain at least 30 entries, then using a for loop in Initialize. This allowed for the possibility that a desperate user would edit the history file and remove some of the lines. Since editing a file of dictionary is impossible, we return to the simpler code:

for dayNum in range(HISTORYSIZE):
readln(historyFile, recentHistory[dayNum]["fat"],
recentHistory[dayNum]["calories"])

Three revisions are necessary. The historyFile parameter is now of type HistoryFileType. The read function must be used instead of readln, since input is no longer coming from a text file. Instead of reading the fat and calorie components separately, the code reads each dictionary from the file in its entirety. Here is the rewritten code:

for dayNum in range(HISTORYSIZE):
read(historyFile, recentHistory[dayNum])

The Update function is revised in similar ways: historyFile is defined as a HistoryFileType, and read and write statements taking a file and a dictionary argument are used instead of input and print.

Stop & Help

Modify Update as just described.

How is the date of the current entry computed?

Next the current date must be determined, by computing the successor of the date in recentHistory[0 ], and then printed. A function to find the date, which uses code from The Calendar Shop, is the following:

  1. 11
  2. 12

Chapter 8

Durk Jan de Bruin

def FindSuccessor(current, next):
if current[0] < NumberOfDaysIn (current11,
current[2]):
next["month"] = current11
next["day"] = current[0] + 1
next["year"] = current[2]
elif current[1] < 12:
next["month"] = current[1]+1
next["day"] = 1
next["year"] = current[2]
else:
next["month"] = "JANUARY"
next["day"] = 1
next["year"] = current[2] + 1

What changes to the top-level decomposition are necessary?

A remaining question is where to put the code that determines the current date and stores it in a file entry. The original decomposition was

ReadEntry (entry)
Initialize (historyFile, entry, recentHistory)
PrintAverages (recentHistory)
PrintGraph (recentHistory)
Update (historyFile, recentHistory)

However, we would like to inform Terry of the current date before he starts entering data. That suggests that the first two function calls be reversed:

Initialize (historyFile, entry, recentHistory)
ReadEntry (entry)

The entry to be read, however, is recentHistory[0], which suggests the following code:

Initialize (historyFile, recentHistory)
ReadEntry (recentHistory[0], recentHistory[0]["date"])

In this decomposition, Initialize would fill elements 1 through 30 of recentHistory,and ReadEntry would be given an entry to fill and a date to print and store into that entry.

Stop & Predict

What do you think of this decomposition?

The problem with this code is that it increases the number of places in the program that access recentHistory directly. Our design all along has aimed to minimize and localize such accesses. In the original program, neither the main program nor ReadEntry needed to know exactly how recentHistory was represented, and we would like to maintain that localization.

Stop & Predict

How should the code be organized to limit direct accesses to recentHistory?

The solution is to call ReadEntry from inside Initialize. The code below would immediately follow the for loop:

next_date = FindSuccessor (recentHistory [HISTORYSIZE-1] ["date"], today)
print('The food data you are about to enter is assumed to be for ', end = '')
print("{}{}{}".format(next_date ["day"], next_date["month"], next_date["year"]))
print('Quit this program and run the history file editor, if this is incorrect.')
ReadEntry(historyFile, today)

ReadEntry must still be modified to take an extra argument, the date to be stored in the entry.

Designing a function WritelnDate to print the date is straightforward. It together with the other code just designed, appears in the Python Code section. The call diagram appears in Figure 8.1 subprograms that were revised or added are boxed.

How is the code tested?

The initializing and updating code is tested as in You Are What You Eat. Other tests should exercise all parts of the FindSuccessor function; thus we should provide test files whose most recent entry occurs in the middle of a month, at the end of a month, and at the end of the year.

The complete program is over 500 lines long. A single DEBUGGING true or false switch isn't enough; it would generate either no output or too much. What we'd like is a selective debugging facility that could be used to generate debugging output only for a particular section of code.

One way to do this is to define the DEBUGGING constant as an integer interpreted as an indicator of which code is being debugged. A value of 1 might mean that the history file operations are being debugged, 2 might mean the date code, and so on. We would define constants to give names to these values and then include code like

if DEBUGGING = HISTORYFILEOPS:
...

  1. 13
  2. 14

Chapter 8

Durk Jan de Bruin

A problem with this approach is that DEBUGGING can take on only one value at a time. We have used the debugging switch not only to produce extra output, but also to cause input to be read from a test file rather than the terminal. It is likely that we'd want both to happen simultaneously during some test runs.

For this program, we choose a simpler approach, that of having more than one debugging constant. The ones used in the Python Code section are listed below.

FILEINPUT - true if test data should be read from a file, false if it should be read from the terminal

DEBUGHISTORY - true if we're debugging operations for reading from and writing to the history file

DEBUGFOOD - true if we're debugging operations for searching the food file

DEBUGDATES - true if were debugging code for manipulating dates

Testing

8.9 Make a table of tests needed for the program and indicate the values that would be tested.

Modification

8.10 Modify the program to print both the day (e.g. Sunday or Monday) and the date of the next entry.

Modification

8.11 Modify the program to ask the user to verify the next date and to enter a different date if necessary.

Modification

8.12 Add a function called FoodMatch to this code that searches foodinfo for the first five letters of a food name and asks the user if the correct entry has been located.

Modification

8.13 Modify the program to accept and process fractional serving sizes. What are the options for representing the history entries?


High-Level Design of the History Editor

What commands will be provided in the history file editor?

To edit the history file Terry needs to add, change, and delete information. Changing is really no more than deleting and adding, so to keep things simple we include only the add and delete commands:

add date

Add an entry for the given date. The program will request two integer values, one for fat and one for calories. It is an error if there is already an entry with the given date in historyFile.

delete date

Delete the entry for the given date. The program will print a message giving the date deleted and the values for calories and fat. It is an error if there is no entry with the given date in historyFile.

Stop & Predict

What other commands might users want?

Terry will also wish to check to be sure an entry is accurate. Commands that allow segments of the history file to be displayed will help here:

list

Print all entries in historyFile.

list date

Print the entry for the given date; print an error message if there is no entry in historyFile for the date.

list start-date end-date

Print all entries in historyFile with dates between those specified; print an error message if no entries are in the specified range.

We figure the user may forget the commands so we provide help with a help command:

help

Print a list of legal commands.

Finally, a quit command allows the user to signal that the editing is complete.

quit

Exit the program.

We make a note to ask Terry if this is a reasonable set of commands.

Questions for Terry

  • Do you (or will you ever) have a file named graphFile?
  • Do you (or will you ever) have a file named foodInfo?
  • Is it OK for the program to display the date it will store for the current entry and have you verify it?
  • Are the commands we've designed add, delete, list, help, and quit sufficient for your purposes?

Stop & Predict

How should users enter dates into the program?

The format of a date will be the usual day-month-year. We need to make clear to Terry whether a legal year should be 19 or 2019. For now, we'll assume the latter.

  1. 15
  2. 16

Chapter 8

Durk Jan de Bruin

Stop & Consider

Which do you prefer, two-digit years or four-digit years? Why?

How is the history file editor organized?

We will organize the program as an instance of the "input and process until done" template, where the input step will read a command from the user and the process step will execute the command. In pseudocode, the main program will appear as follows:

initialize
repeat
get a command
execute the command
until no more commands
finish up

This kind of program is common enough to have a name: command interpreter. ("Interpreting" a command means executing it.)

How are the history entries represented?

As we did in You Are What You Eat, we delay considering the procedural decomposition any further until we decide how to represent the collection of history entries in the program.

Stop & Predict

What are the options for representing the history entries?

Currently, the history entries are stored in a disk file. One option is to retain this implementation and represent the collection internally as a Python file. Another option is to use an array. It is possible even likely that the representation we choose will have to be changed in some future version of the program. Designing first around the abstraction of the collection of entries, then choosing an implementation, will make changing the implementation much easier.

How do we create an abstract view?

To get a comprehensive idea of the representation we need, we build "views" of the collection of history entries and the actions that will be performed on them. Views are perspectives on the actions of the program on the data objects. Generally, the least detailed view is the view the user has of the editor commands acting on the entries. The programmer's view of the actions of the program on the data is likely to be more detailed and more precise than the users. In this case the programmer views a sequenced collection of entries that can be searched, traversed, and added to, whose size can be determined, and each of which has a date, a calorie entry, and a fat entry that can be modified. A third view is the programmer's detailed view where the particulars of the Python constructs are considered.

In the abstract view, we are concerned with a sequence of history entries, ordered by date, most recent first. The commands can be translated into the following abstract operations:

  • Initialize the sequence.
  • Find the position in the sequence of the entry with a given date or the position in the sequence where an entry with that date would go.
  • Insert a new entry at a given position in the sequence
  • Delete the entry at a given position in the sequence.
  • Print all entries in the sequence
  • Print all entries between two given dates in the sequence.

The operations are complete, yet simultaneously they provide the flexibility of implementing the sequence either as a file or as an array.

Stop & Predict

Explain why a file or an array would work with these operations.

What operations have we left out?

Operations for comparing dates will also be necessary. We'll postpone thinking about them for now.

How are the commands and abstract operations related?

For each of the commands, here is its action in terms of the abstract operations:

add date

Find the position of an entry with the given date. If an entry with that date already exists, signal error; otherwise construct a new entry and insert it at that position.

delete date

Find the position of an entry with the given date. If an entry with that date does not exist, signal error; otherwise delete the entry.

list

Print the information in all entries.

list date

Find the position of an entry with the given date. If an entry with that date does not already exist, signal error; otherwise print the information in the entry.

list start-date end-date

Find the position of an entry whose date is the same as or preceding end-date. Then print the information in all entries between end-date and start-date.

  1. 17
  2. 18

Chapter 8

Durk Jan de Bruin

Is a file or an array best?

For this program, we choose to implement the collection as a Python file. First, the size of the collection is unknown, so we cannot easily define an array that will accommodate all the data. Perhaps Terry will collect years of data. Second, all we want to do is put together a prototype program quickly, and a file will work fine for this purpose. Terry will then use it, and we will decide what needs to be improved based on his feedback.

We focus now on coding the six abstract operations - initialize, find the position of an entry, insert a new entry at a given position, delete the entry at a given position, print all entries, and print a given range of entries using Python files.

Modification

8.14 Suppose Terry found that he frequently needed to exchange the information for one date with the information for another date. Describe commands that might be added to make this easy, and explain how they would be implemented in terms of the abstract operations already designed.

Analysis

8.15 A user might find it easier to understand a list command with only one variation rather than three. Explain how one can produce the output of the zero-argument and one-argument variations of the list command by using the two-argument list command.

Reflection

8.16 Would it be better to have just one variation of the list command or three as we have proposed. Explain.

Analysis

8.17 Describe the advantages of using an array rather than a file for the collection of fat and calories.


Implementing the Abstract Operations

How is the "find" operation coded?

All of the intended commands will use the "find" operation, so we'll work on that first. The "find" operation will take as parameters the file and the date whose entry is to be located. Moreover, it must return an indication of whether or not the entry is found, and the position at which it is found. Thus the function header will be

def Find (history, date, found, position):

The history file is passed as a variable parameter, both because Python does not allow files to be passed by value and because, should the history be stored in an array, passing it by value would result in the inefficiency of copying the array.

Stop & Predict

What is the position of an entry in a file?

The meaning of a position is obvious with an array; its meaning for a file is much less clear, so we postpone this problem for now.

How is a file searched?

Since elements of files are accessed sequentially, the "find" operation requires a linear search. The code for searching a file is similar to code for searching an array, so we can adapt the searching template from Is illegal? and Space Text. Recall that the entries in the file are stored most recent first; thus finding the position where an entry with the given date would go means finding the first entry in the file with a date that's the same as or earlier than the given date.

foundAtOrBefore = False
numdates = 0
myFile = open('historyFile.txt', 'r')
for lines2 in myFile:
numdates = numdates + 1
for i in range(numdates):
if entry's date is at or before the given date:
foundAtOrBefore = True
break
return found, position

At the end of this loop, there are three possibilities: foundAtOrBefore is false, so Find should return failure; foundAtOrBefore is true but the entry date is earlier than the given date, so Find should return failure; or foundAtOrBefore is true and the entry date is identical to the given date, so Find should return success. Thus the following code must be added:

if foundAtOrBefore:
set found to True if date and entry["date"] are
the same, and to false otherwise
else:
found = False

What happens when an entry is found?

When an entry is found, it may be deleted, or an entry may be added resulting in a change to the file. Either requires the file to be copied; only the list command requires finding an entry without modifying the file. Thus, it seems efficient to build the copying into the operation of finding an entry:

foundAtOrBefore = False
found = False
numdates = 0
position = 0
myFile = open('historyFile.txt', 'r')
for lines2 in myFile:
numdates = numdates + 1
for i in range(numdates):
if entry's date is at or before the given date:
foundAtOrBefore = True
found = True
position = i
break
return found, position

  1. 19
  2. 20

Chapter 8

Durk Jan de Bruin

if foundAtOrBefore:
set found to True if date and entry["date"] are
the same, and to false otherwise
else:
found = False

The history entry whose date is at or before the given date will not have been written to the temporary file when Find returns.

The temp file will need to be used by other file operations, and thus it should be a parameter. This seems undesirable, since if the sequence of history entries were to be represented as an array instead of a file, it would be inappropriate to have such a parameter. An intriguing thought is to use the temp file as the position parameter. We'll try that. Thus temp replaces position in the parameter list, and PositionType, the type of a "position" in the sequence, will be defined to be file of EntryType, just like Sequence-Type.

How is the "insert" operation coded?

The "insert" operation will assume that a "find" has just been done. Its parameters will be the history, the entry to be inserted, and the place to put it:

def Insert(history, entry, temp):

Insertion into the history means writing it to the temporary file, copying the rest of the file to the temporary file, then copying the whole file back to the original. (We did this in You Are What You Eat) The delete operation will need to perform a similar operation, so we put the copying and copying back code into a separate function:

original = open("historyFile.txt", "w+")
for line in entry:
original.write(line)
original.close()
CopyThenCopyBack (history, temp)

Copying is easy:

def CopyThenCopyBack (original, temp):
entry1 = EntryType.copy()
original = open("historyFile.txt", "r")
lines = original.readlines()
original.close()
temp = open("temp.txt", "w+")
for line in lines:
temp.write(line)
temp.close()

temp = open("temp.txt", "r")
lines = temp.readlines()
temp.close()
original = open("historyFile.txt", "w+")
for line in lines:
original.write(line)
original.close()

How is the "delete" operation coded?

Delete is similar to Insert. It also assumes that Find has just been called:

def Delete(history, temp):
CopyThenCopyBack (history, temp)

How is the "print all entries" operation coded?

Printing all the history entries is another applieation of the "input and process until done" template:

def PrintAll(history):
entry1 = EntryType.copy()
historyFile1 = open('historyFile.txt', 'r')
for line1 in historyFile1:
line2 = line1.split()
date = line2[0]
fat = line2[1]
calories = line2[2]
fat1 = int(fat)
calories1 = int(calories)
entry1["date"] = date
entry1["fat"] = fat1
entry1["calories"] = calories1
PrintEntry(entry1)

Stop & Predict

What patterns can be recycled to create the function that prints the entries between two dates?

How is the "print all entries between two given dates" operation coded?

The "print all entries between two given dates" operation is done after finding an entry whose date is the same as or preceding the later of the two dates. (Recall that the entries are arranged most recent first in the file.) Subsequent entries are read and printed until an entry preceding the earlier of the two dates is encountered. The code is almost exactly the same as the code for the first version of Find:

def PrintUpTo(history, date, temp):
done = False
entry = EntryType.copy()
while not done:
if AtOrBefore(date, entry["date"]):
PrintEntry(entry)
else:
done = True

Note that a position parameter is necessary to be consistent with the other functions that follow a call to Find.

Stop & Help

Find the bug in the above code.

How is the "initialize" operation coded?

Last is the "initialize" operation. Since every operation that reads through the file starting at the beginning does its own reset, no initialization is necessary. Thus the corresponding function is empty:

def Initialize (history):
pass

  1. 21
  2. 22

Chapter 8

Durk Jan de Bruin

Stop & Predict

What sort of functions are best for comparing the dates?

What about the code that manipulates dates?

Find and PrintUpTo both involve comparison of two dates. We will use two boolean functions: Precedes and SameDate. Recall that a date has three components, a year, a month, and a day. A date stored in date1 is earlier than a date stored in date2 if date1's year is earlier than date2's, if the years are the same and date1's month is earlier than date2's, or if the years and months are the same and date1's day is earlier than date2's. Two dates are the same if all their components are equal. Here's the code for the two functions:

def Precedes(date1, date2):
if date1["year"] < date2["year"]:
Precedes = True
elif date1["year"] > date2["year"]:
Precedes = False
elif date1["month"] < date2["month"]:
Precedes = True
elif date1["month"] > date2["month"]:
Precedes = False
else:
Precedes = date1["day"] < date2["day"]
return Precedes

def Same(date1, date2):
Same = (date1["month"] == date2["month"]) and (date1["day"] == date2["day"]) and (date1["year"] == date2["year"])
return Same

(Standard Python does not allow comparison of entire dictionary.)

How can the code be tested?

The routines written so far can be combined with a driver program and tested. We do this now.

The first development step is to test the file printing routines. Once working, they provide a way to test the other code. The test data for the file handling code is similar to what would be used with an array: check the middle values along with the boundary values. Here, we are interested in values not in the file as well as those in the file, so we test PrintUpTo with the latest and earliest dates in the file as well as dates following the latest date and preceding the earliest date. A diagram representing these test cases appears above.

What bugs are encountered?

A bug arises from a mismatch between the program that created the file and the driver program used for testing. In the driver program, MonthType is declared as integer to simplify input of test values, while in the creation program MonthType is declared as an enumerated type. Most Python environments do no error checking at all for input from non-text files, assuming that they will be read in the same way they were written. Any source of inconsistency will cause errors. This may be regarded as a disadvantage of this representation.

Testing also reveals a bug in PrintUpTo; the symptom is that one too many entries are getting printed. The problem turns out to be that the first entry outside the requested range was printed before it was checked. We rearrange the code to put the call to PrintEntry inside an if statement, but then we get the comparison backward. One more test run reveals this bug and leads to the code in the Python Code section.

Find is next to be tested, since the other operations use it. Test values are similar to those used with PrintUpTo. Finally all the other operations are tested; again we focus mainly on boundary values.

While testing and analyzing the code just written, we realize that the decision to store the file in reverse order introduces an extra level of confusion. We have to worry not only about the search test being off by one entry, but also about the direction of the error. We resolve to be especially careful in checking for errors in operations that search the file, and to look for an opportunity to simplify the code in future revisions of the program.

Analysis

8.18 Should Find always search the entire file? Why or why not?

Modification

8.19 Modify the Initialize function to check that dates of file entries are ordered from most recent to least recent and to print a warning message if they are not.

8.20 Modify the Initialize function to check that there are no duplicate dates in history entries and to print a warning message if there are entries with duplicate dates.

Reflection

8.21 List bugs that programmers should anticipate when writing routines that process files.

  1. 23
  2. 24

Chapter 8

Durk Jan de Bruin

Designing and Developing the Remainder of the History Editor

What remains to be designed?

Now that the routines to manipulate the history data structure are designed and tested, we can proceed to the main program and the command scanner.
Recall that we decomposed the main program as follows:

initialize
repeat
get a command
execute the command
until no more commands
finish up

Hidden in that decomposition are such details as reading characters from the input, isolating and translating commands and date arguments, and checking for errors.

What should the "get a command" step do?

Getting a command is an ambiguous step. How much work should the "get a command" step do? Here are two possibilities at opposite ends of the spectrum.

Get just the first word of the command: Read characters until the first word of the command is isolated, then check that word. Once the command word is recognized, subsequent input and processing is handled in the "execute the command" step.

Get a complete legal command: Read the complete command including its arguments, check it for errors, and translate it into an internal form. The "execute the command" step then performs only the appropriate history file operations.

Stop & Predict

What are some alternatives for the "get a command" step between these extremes?

What worked before?

In is it Legal? we used the "read one, process one" template for input and found it advantageous to read a whole line of input before processing. This suggests a third possibility, which also eliminates checks for end-of-line after the input step.

Get the line, process the command word: Read an entire line, then isolate the first word on the line and make sure it specifies a legal command. Subsequent isolation of arguments on the line is handled in the "execute the command" step.

This seems better than the "get just the command word" alternative.

How do we select an alternative?

The choice between "get the line, process the command word" and "get a complete legal command" consists mainly in deciding how much to analyze the commands in the "get a command" code. The goal is to make the code clear and to avoid unnecessary repetition.

The decision will depend on the form of the commands. Similar commands can be analyzed by the same code; commands that differ in form must be analyzed separately.

In addition, the code for command execution will be clearer if error checking is done when the command is read. Error checks in the command execution routines can obscure the processing performed by the routines. However, if error checks and command interpretation are similar, they are best combined in the command execution step.

The choice of how much processing to do in the "get a command" step arises in the design of any command interpreter. To decide for this program, we gather some more information about the commands.

Stop & Predict

What common command errors will need to be checked?

What errors will the program check?

To determine where to put the error-checking routines, we make a list of the possible errors in the arguments to the various commands.

list
too many arguments (3 or more) are specified
one of the arguments isn't a date
there is no history entry with the given date
there is no history entry in the given range of dates

add
too many arguments (2 or more) are specified
too few arguments (0) are specified
the argument isn't a date
a history entry with the given date already exists

delete
too many arguments (2 or more) are specified
too few arguments (0) are specified
the argument isn't a date
no history entry with the given date exists

help
no errors are possible

quit
no errors are possible

What are the pros and cons of each decomposition?

Examining the possible errors does not completely clarify what the "get a command" step should do. The number of arguments varies with the command, arguing against error checking in the "get a command" step.

  1. 25
  2. 26

Chapter 8

Durk Jan de Bruin

On the other hand, all the arguments are dates, so it makes sense to have a single routine to check that each date argument is legal.

The choice is between "get the line, process the command word" and "get a complete legal command." We decide to design both solutions and compare them.

Analysis

8.22 In this design the output requested by the user is deduced by examining the arguments to the list command. Instead, one might provide several different list commands such as listAll, listSome, and listOne. Explain the advantages and disadvantages of each approach.

Reflection

8.23 Given the nature of novice computer users, what kinds of help besides a list of legal commands might be added to the program? Justify each suggestion.

Analysis

8.24 Provide a pseudocode description of an interactive interface for this program that asks the user for a date only if the command requires date information. What are the advantages of this approach?


The "Get the Line, Process the Command Word" Design

What will Get Command and Execute Command do?

In the "get the line, process the command word" design, the GetCommand function will start by reading a line. It will then isolate the first word in the line, make sure it's a command, and return. Depending on the command, ExecuteCommand will call one of the specialized execution functions ExecuteList, ExecuteInsert, ExecuteDelete, ExecuteHelp, or ExecuteQuit. Some of these will isolate the following word(s) on the line, convert them to dates, and apply the appropriate history file operations.

How will GetCommand and ExecuteCommand communicate?

GetCommand and ExecuteCommand both need to access the line containing the command. Thus the line will need to be either a parameter to both or a variable global to both. To make clear just what is getting communicated to what, we will make the line a parameter to both functions.

ExecuteCommand will need to know the position in the line at which to start looking for arguments. This might also be passed as a parameter. Rather than store it separately from the line, however, we will combine it with the line in a dictionary.

Stop & Help

Write a declaration for the dictionary that will hold the line and the position.

How will ExecuteCommand work?

ExecuteCommand conceptually is an application of the "select from alternatives" template. In earlier case studies we used an if-else statement to select alternatives. Python does not allow the use of strings as case selectors. One solution is to define an enumerated type with one element for each command, since members of an enumerated type can be used as case selectors. GetCommand can then translate the isolated command word into a member of that type before returning.

What are the headers for these functions?

This solution leads to the following type definitions and function headers for GetCommand and ExecuteCommand:

LineType = {
"chars": '',
"length": 0,
"position": 0
}
CommandType = ["LISTCMD", "ADDCMD", "DELETECMD", "HELPCMD", "QUITCMD", "UNKNOWN"]

def GetCommand (line, command):

def ExecuteCommand (line, command, history, status):

What is a consistent design for the Get... functions?

Note that there is a lot of "getting" going on: "getting" a line, "getting" a command, "getting" an argument, and so on. It makes sense to have the corresponding functions be as similar as possible. Each should have a parameter of the type being "gotten."

Stop & Help

Create informative names for the parameters that each function "gets".

What about illegal commands?

Suppose that there is an error in the user's command. Each "get" routine must have a way of indicating to its caller that there was nothing to get or that the thing being gotten was of the wrong type.

In Is It Legal? a parameter of type boolean was used to indicate an error. In this situation, there are two types of error the program should indicate:

  • error because there is nothing left on the line or in the input
  • error because there is something on the line or in the input, but it is of the wrong type

Stop & Predict

List the specific errors that the program might detect on the line.

How should the errors be indicated?

To indicate the status of the processing of the line we provide an extra parameter to each of the Get... routines. We define an enumerated type whose members are status values. They represent errors detectable either in GetCommand or ExecuteCommand:

SUCCESS - The command could be completed successfully.
NOINPUT - GetLine found no line to return (it encountered end-of-line on input).
LINETOOLONG - GetLine found too many characters to read on the line.

  1. 27
  2. 28

Chapter 8

Durk Jan de Bruin

NOWORD - GetWord found no further things on the line to read.
WORDTOOLONG - GetWord found a word longer than it had room for.
NOCOMMAND - GetCommand found a blank line.
BADCOMMAND - The first word on the line was not a legal command.
NODATE - The last thing on the line has been read.
TOOMANYDATES - Too many arguments were specified on the command line.
BADDATE - An argument isn't a correctly formatted date.
ALREADYTHERE - A request to add an entry duplicated an existing date.
NOTTHERE - A delete or list request specified a non-existent entry.
EMPTYRANGE - A list request specified a range of dates that contained no history entries.

Here are revisions to the type definitions and function headers provided so far:

StatusType = ["SUCCESS", "NOINPUT", "LINETOOLONG", "NOWORD", "WORDTOOLONG", "NOCOMMAND", "BADCOMMAND", "NODATE", "TOOMANYDATES", "BADNUM", "BADSEP", "EXTRAJUNK", "BADDATE", "ALREADYTHERE", "NOTTHERE", "EMPTYRANGE"]

def GetCommand (line, command, status):

How is GetCommand coded?

The code for GetCommand is now straightforward

def GetCommand(line, command, status):
print('COMMAND? ')
GetLine(line, status);
if status == "NOINPUT":
command = "QUITCMD"
elif status == "SUCCESS":
GetCmdWord(line, command, status)

The GetLine and GetCmdWord functions are similar to functions used in Is It Legal? and Space Text.

def GetLine(line):
global status
status = "SUCCESS"
line["chars"] = input()
line["length"] = len(line["chars"])
line["position"] = 1
if line["chars"] == '':
status = "NOCOMMAND"
if line["length"] > MAXLINELEN:
status = "LINETOOLONG"
line["chars"] = ''

def GetCmdWord(line):
global command, status
status = "SUCCESS"
command = "UNKNOWN"
word = GetWord(line)
if status == "NOWORD":
status = "NOCOMMAND"
elif word == LISTWORD:
command = "LISTCMD"
elif word == ADDWORD:
command = "ADDCMD"
elif word == DELETEWORD:
command = "DELETECMD"
elif word == HELPWORD:
command = "HELPCMD"
elif word == QUITWORD:
command = "QUITCMD"
else:
status = "BADCOMMAND"

Stop & Help

Write the GetWord function. Its arguments are the line, a packed array of characters, and a status. It will return the next word from the line in the array and update the line position appropriately.

How does the design of GetCommand affect the main program?

GetCommand returns either with status = SUCCESS and command containing a translated command or with status indicating some error and command = UNKNOWN. The next question is where to handle the latter case. If status does not equal SUCCESS, code to print an error message could be added to the end of GetCommand. Or the main program could contain this code. Or ExecuteCommand might be written to handle an unknown command as well as all the legal commands.

A complicating factor is that one non-SUCCESS value for status, namely NOINPUT, represents an alternative way to quit the program. Thus either the main program will have to examine the contents of status, or GetCommand will have to return a value of QUITCMD in this case.

Somewhat arbitrarily, we choose to complicate both GetCommand and the main program slightly by adding status checks to each. The revised code follows.

def GetCommand(line, command, status):
print('COMMAND? ')
GetLine(line, status);
if status == "NOINPUT":
command = "QUITCMD"
status = "SUCCESS"
elif status == "SUCCESS":
GetCmdWord(1ine, command, status)

# *** main program ***
Initialize(history)
done = False
while command!= "QUITCMD":
GetCommand(line)
print(status)
print(line)
if status == "SUCCESS":
ExecuteCommand (line, history)
if status != "SUCCESS":
PrintErrorMsg (status)

  1. 29
  2. 30

Chapter 8

Durk Jan de Bruin

The main program uses a PrintErrorMsg function to print an error message. It is useful to print all error messages in one place, just in case we wish in a future revision to change their format or keep track of which errors are most commonly made by users.

How is ExecuteCommand designed?

Given a legal command, ExecuteCommand merely needs to decide which command it is and then execute it. A straightforward decomposition is to have a different execution function for each command. Deciding which of the functions to call can be done in an if-else statement:

if command == "LISTCMD":
ExecuteList(line, history)
if command == "ADDCMD":
ExecuteAdd(line, history)
if command == "DELETECMD":
ExecuteDelete(line, history)
if command == "HELPCMD":
ExecuteHelp(line, history)
if command == "QUITCMD":
ExecuteQuit(line, history)

ExecuteHelp and ExecuteQuit don't really need any arguments, but we code them the same as the other execution functions for consistency.

How is ExecuteList designed?

ExecuteList is the hardest of the execution functions, since it may take 0, 1, or 2 arguments. Thus we attack it first. It reads dates from the commandline, then performs the appropriate operation on the history file.

Reading the command arguments will be done using a GetDate function whose parameters are the line, a date to be returned, and a status. Date arguments will be counted and stored in an array of size 2. The process is very similar to that of reading a line.

The main difference is that eoln rather than read is called to see if there are no more characters, but GetDate is called to find that there are no more dates. Here's the code:

def ExecuteList(line, history):
global status
dates = []
for i in range(2):
dates.append (DateType.copy())
tempDate = DateType.copy()
numDates = 0
done = False
found = False
position = 0
k = 0
entry = EntryType.copy()
words = line["chars"].split()
words.pop(0)
numDates = len(words)
if numDates > 2:
status = "TOOMANYDATES"
done = True
else:
for dates1 in words:
date1 = dates1.split("-")
dates[k]["day"] = int(date1[0])
dates[k]["month"] = int(date1[1])
dates[k]["year"] = int(date1[2])
k = k + 1
for dates1 in dates:
if not LegalDate(dates1["month"], dates1["day"],
dates1["year"]):
status = "BADNUM"
if numDates == 0:
PrintAll(history)
status = "SUCCESS"
if numDates == 1:
found, position = Find(history, dates[0], found,
entry, position)
if found:
PrintEntry1 (history[position])
status = "SUCCESS"
else:
status = "NOTTHERE"
if numDates == 2:
numdates = 0
found1, position1 = Find(history, dates[0],
found, entry, position)
found2, position2 = Find(history, dates[1],
found, entry, position)
if found1 and found2:
PrintEntry1 (history[position1])
myFile = open('historyFile.txt', 'r')
for lines2 in myFile:
numdates = numdates + 1
for i in range(numdates):
if history[i]["date"][0] ==
dates[0]["day"] and history[i] ["date"][1]
== dates[0]["month"] and history[i]
["date"][2] == dates[0]["year"]:
continue
if history[i]["date"][0] == dates[1]
["day"] and history[i]["date"][1]
== dates[1]["month"] and history[i]["date"]
[2] == dates[1]["year"]:
continue
if dates[0]["year"] < history[i]["date"][2]
< dates[1]["year"]:
PrintEntry1 (history[i])
elif history[i]["date"][2] == dates[1]
["year"]:
if dates[0]["month"] < history[i]
["date"][1] < dates[1]["month"]:
PrintEntry1 (history[i])
elif history[i]["date"][1] ==
dates[0]["month"] or history[i]
["date"][1] == dates[1]["month"]:
if dates[0]["day"] > dates[1]["day"]:
if dates[0]["day"]< history[i]
["date"][0] or history[i]["date"]
[0] < dates[1]["day"]:
PrintEntry1 (history[i])
elif dates[0]["day"] < dates[1]["day"]:
if dates[0]["day"]<= history[i]
["date"][0] < dates[1]["day"]:
PrintEntry1 (history[i])
PrintEntry1 (history[position2])

  1. 31
  2. 32

Chapter 8

Durk Jan de Bruin

How is ExecuteAdd coded?

ExecuteAdd will be easier to code, since the add command takes exactly one date argument. The argument will be retrieved from the line, the line will be checked to make sure no more dates have been provided, the code from the revised You Are What You Eat will be inserted to collect a day's data from the user, and then the appropriate history file operation will be applied. Here's the code:

def ExecuteAdd(line, history):
global status
date = DateType.copy()
tempDate = DateType.copy()
entry = EntryType.copy()
dates = []
for i in range(2):
dates.append (DateType.copy())
found = False
done = False
k = 0
position = 0
inFile = ''
words = line["chars"].split()
words.pop(0)
numDates = len(words)
if numDates > 2:
status = "TOOMANYDATES"
done = True
else:
for dates1 in words:
date1 = dates1.split("-")
dates[k]["day"] = int(date1[0])
dates[k]["month"] = int(date1[1])
dates[k]["year"] = int(date1[2])
k = k + 1
if not LegalDate(dates[0]["month"], dates[0]
["day"], dates[0]["year"]):
status = "BADNUM"
found, position = Find(history, dates[0],
found, entry, position)
if status!= "BADNUM":
if not found:
dates[0]["month"] = MonthEquivalent1
(dates[0]["month"])
ReadEntry (inFile, dates[0])
dates[0]["month"] = MonthEquivalent
(dates[0]["month"])
Initialize (history)
else:
status = "ALREADYTHERE"

How are the remaining Execute routines coded?

ExecuteDelete is even simpler than ExecuteInsert. The coding is straightforward.

Stop & Help

Code the ExecuteDelete function.

ExecuteHelp merely prints a helpful message about all the commands. ExecuteQuit need do nothing, but perhaps a message saying something like "Exiting history file editor" would be reasonable.

How is the GetDate function designed?

All that's left is the GetDate function. Recall that the format of a date is day-month-year. GetDate must retrieve and analyze the components of the date from the line. For consistency with GetCmdWord, GetDate will first call GetWord to isolate the next argument from the line, and then work with the isolated word.

What are possible errors in a date?

To aid in decomposing GetDate, it will help to list all the possible errors that can appear in a non empty date

  • It doesn't start with digits
  • It starts with digits that represent a value less than 1 or greater than 12.
  • The digits aren't followed by a hyphen.
  • The hyphen isn't followed by digits.
  • The digits following the hyphen represent a value less than 1 or greater than the number of days in the month.
  • The digits are followed by something other than a hyphen.
  • The second hyphen isn't followed by digits.
  • The remaining digits represent a value that isn't a legal year.
  • A year is followed by anything other than the end of the word.

How is GetDate decomposed?

We could process the date word with a sequence of character accesses and tests, but this would be clumsy. Another option is to provide functions to get an integer and a hyphen, analogous to the other Get... functions. This leads to the following code:

GetInteger (word, int, status)
if status == "NOINT":
status = "BADDATE"
elif not (range(1,13)):
status = "BADDATE"
else:
GetHyphen (word, status)
if status == "NOHYPHEN":
status = "BADDATE"
else:
GetInteger (word, int, status)
if status = "NOINT"
status = "BADDATE"
else:
.
.
.

  1. 33
  2. 34

Chapter 8

Durk Jan de Bruin

Stop & Help

Complete the code just outlined.

This is clumsy. Extra tests could reduce the nesting of the code as follows:

GetInteger (word, int, status)
if status == "NOINT":
status = "BADDATE"
elif not (range(1,13)):
status = "BADDATE"
if status == "SUCCESS":
GetHyphen (word, status)
if status == "NOHYPHEN":
status = "BADDATE"
if status = "SUCCESS"
.
.
.

Stop & Help

Complete the code above.

This is still clumsy. Some sort of loop is preferable. We try a state-based approach similar to that used in Is It Legal? but it is too complicated. Sometimes a brute-force approach is best of all, despite its inelegance; this seems to be such a situation. The following code, in which checks of form (for example, that the month has 1 or 2 digits) precede checks for content (for example, that the month value is at most 12), results.

def GetDate (line, date, status):
word = ''
index = 0
month = 0
day = 0
year = 0
GetWord(line, word, status)
if status == "NOWORD":
status = "NODATE"
if status == "SUCCESS":
index = 1
GetOneOrTwoDigits (word, index, month, status)
if status == "SUCCESS":
GetSeparator (word, index, status)
if status == "SUCCESS":
GetOneOrTwoDigits (word, index, day, status)
if status == "SUCCESS":
GetSeparator (word, index, status)
if status == "SUCCESS":
GetFourDigits (word, index, year, status)
if status == "SUCCESS":
CheckNoMore (word, index, status)
if status =="SUCCESS":
if not LegalDate (month, day, year):
status = "BADDATE"
else:
date["month"] = MonthEquiv(month)
date["day"] = day
date["year"] = year

Each Get... function above checks for the specified eharacters, increments index if it finds them, and stores an error value in status if it does not. For example, here's the code for GetOneOrTwoDigits:

if IsDigit(word[index]) and not
IsDigit (word[index + 1]):
n = DigitValue (word[index])
index = index + 1
elif IsDigit(word[index]) and IsDigit (word[index + 1]):
n = 10 * DigitValue (word[index]) + DigitValue
(word[index + 1])
index = index + 2
else:
status = "BADNUM"

Not pretty, but it works. We also invent several new status values, and add corresponding error messages to PrintErrorMsg:

BADNUM - Either the month, the day, or the year is incorrectly formatted.
BADSEP - A hyphen (separator) was not found where expected.
EXTRAJUNK - There were nonblank characters after the year.

That completes the program.

Stop & Help

Produce the call diagram for the program just completed.

How is all this code tested and debugged?

The file-manipulating routines were tested previously, so what remains are the functions to get a date and execute commands. We test GetDate separately (together with subprograms IsDigit, DigitValue, GetWord, GetLine, WriteLnDate, LeapYear, LeapDay, NumberOfDaysIn, and the function GetDate calls directly) with a program that repeatedly reads a date and prints it. With test data, we make sure that boundary dates at the start and end of months are handled correctly and that values with one too many or one too few digits are detected.

Finally, we test the remainder of the program. Unlike code in earlier ease studies, this program does not include a DEBUGGING switch, since we feel that the list command provides as much information as necessary to detect bugs.

Application

8.25 Add a command called Copy to the "get a line, process a command word" version that copies the fat and calorie information from the history file entry for one date as the fat and calorie information for another date. Change the program as little as possible.

Analysis

8.26 Suppose a boolean function NoMoreWords had been written that returned true exactly where there were no more words on its line argument. In which routines should NoMoreWords be called?

Modification

8.27 Write the NoMoreWords function, and modify the program to call it where appropriate.

  1. 35
  2. 36

Chapter 8

Durk Jan de Bruin

Reflection

8.28 Compare the brute-force approach to the attempt at elegance in processing dates. What features of a date make the brute-force approach reasonable?

Testing

8.29 Design a collection of test cases for GetDate. Describe why each is necessary and explain why this set of cases will adequately test the routine.


The "Get a Complete Legal Command" Design

What are the differences between the two designs?

Having implemented the "get a line, process the command word" version of this program, it is easy to convert the design to "get a complete legal command." The only real difference is that dates must be isolated and analyzed in the GetCommand function rather than in the ExecuteCommand function. There will be several changes in details, however.

Stop & Predict

What functions will need to be modified?

How will the main program change?

First, the main program will change. ExecuteCommand will no longer need the line as an argument, since GetCommand will have completely processed the line. (It will still need a status argument, since there are still errors to detect after the command is read.) The command will now include the arguments as well as the command word; thus a new definition is provided for CommandType:

CmdWordType = [LISTCMD,..., UNKNOWN] # the old CommandType
CommandType = {
"cmdWord": '',
"numArgs": 0,
"args": [DateType.copy(), DateType.copy()]
}

Here's the rewritten main program:

# *** main program ***
Initialize(history)
done = False
while command["cmdWord"] != "QUITCMD":
GetCommand(line)
print(status)
print(line)
if status == "SUCCESS":
ExecuteCommand (line, history)
if status != "SUCCESS":
PrintErrorMsg (status)

How will the Execute functions change?

These changes are propagated to the command-handling routines. Instead of taking a line as a first argument, they take a command. They no longer "get" the arguments themselves; each routine is cut roughly in half as a result. Here, for example, is ExecuteAdd:

def ExecuteAdd (command, history, status):
found = False
entry = EntryType.copy()
position = 0
Find(history, command["args"][1], found,
entry, position)
if found:
status = "ALREADYTHERE"
else:
entry["date"] = command["args"][1]
ReadEntry (entry)
Insert (history, entry, position)

None of the history file operations changes.

How will the new GetCommand be organized?

To design the new GetCommand, we repeat an approach taken in Is It Legal? it applies successively more stringent checks to the contents of the line. The checks follow.

  • Has a nonempty line been entered?
  • Does the line contain at least one word and at most three?
  • Is the first word a command word, and is the number of words that remain consistent with that command?
  • Do the remaining words represent dates?

The diagram below represents how these checks focus increasingly on the line contents.

Assuming that each check is done in a separate function, we code GetCommand as shown below.

  1. 37
  2. 38

Chapter 8

Durk Jan de Bruin

def GetCommand(line):
global status
print('COMMAND? ')
GetLine(line)
if status == "NOINPUT":
command["cmdWord"] = "QUITCMD"
command["numArgs"] = 0
status = "SUCCESS"
if status == "SUCCESS":
GetWords(line)
if status == "SUCCESS":
ConvertToCommand (words)

The type WordArrayType represents an array of the words on the command line. Patterning it on LineType, we supply the following definitions:

MAXCMDLEN = 3
WordArrayType = {
"length": 0,
"words": ['','','']
}
# WordType was defined in the earlier version of the program.

How is the command word analyzed?

GetLine is unchanged. GetWords is coded to resemble GetLine and the argument-reading code from the earlier version of the history editor. ConvertToCommand is patterned on GetCmdWord from the earlier version; it checks in addition that the number of arguments matches the command, as follows:

def ConvertToCommand (words):
global status
status = "SUCCESS"
command["cmdWord"] = "UNKNOWN"
if words["length"] == 0:
status = "NOCOMMAND"
elif words["words"][0] == LISTWORD:
command["cmdWord"] = "LISTCMD"
command["numArgs"] = words["length"] - 1
elif words["words"][0] == ADDWORD:
command["cmdWord"] = "ADDCMD"
if words["length"] < 2:
status = "NODATE"
elif words["length"] > 2:
status = "TOOMANYDATES"
else:
command ["numArgs"] = 1
elif words["words"][0] == DELETEWORD:
command["cmdWord"] = "DELETECMD"
if words["length"] < 2:
status = "NODATE"
elif words["length"] > 2:
status = "TOOMANYDATES"
else:
command ["numArgs"] = 1
elif words["words"][0] == HELPWORD:
command["cmdWord"] = "HELPCMD"
command["numArgs"] = 0
elif words["words"][0] == QUITWORD:
command["cmdWord"] = "QUITCMD"
command["numArgs"] = 0
else:
status = "BADCOMMAND"

How are the arguments analyzed?

ConvertToArgs converts the arguments to dates. It calls a function ConvertToDate once for each argument. ConvertToDate is essentially GetDate from the earlier version without the initial call to GetWord.

Which version is better?

The functions that are different in the two versions appear in the Python Code section. The "get a complete legal command" version is slightly longer than the "get a line, process the command word" version. It should, however, be much easier to add a command to the "get a complete legal command" version, provided that it sufficiently resembles the other commands.

Analysis

8.30 Describe a modification to the program that would be easier to make to the "get a line, process the command word" version than to the "get a complete legal command" version. Explain in general what kinds of changes would be easier to add to each version of the code.

Application

8.31 Create a boolean function called LegalDate that returns true if the correct number of legal dates were input and returns false otherwise. The function should call GetDate.

Modification

8.32 Modify the GetDate function to handle dates entered in the format 1-2-91 as well as in the format 1/2/91.

Testing, Reflection

8.33 Provide a set of test data for the "get a complete legal command" version. Which of the two versions is easier to test?

Reflection

8.34 Which of the two versions is easier for you to understand? Why?

  1. 39
  2. 40

Chapter 8

Durk Jan de Bruin

Outline of Design and Development Questions

These questions summarize the main points of the commentary.

Planning the Modifications
What new features were requested?
What changes will produce a file containing the graphs?
What changes will implement the new input format?
What changes will allow the user to add forgotten data and correct errors?
How might the modifications be incorporated?
How will the history file editor work?
How will the history file format change?
How will the date for the current entry be determined?
What modifications are needed to go with the new file format?
What changes to You Are What You Eat are to be made?
What comes first?

Implementing Changes to You Are What You Eat
How is PrintGraph changed?
How is ReadEntry rewritten?
In which function should the food be looked up?
What is the format of the food information file?
How is a file of dictionary created?
How is the error checking coded?
How is the file search coded?
How are the fat and calorie amounts coded?
How is the code tested?
What modifications are necessary to keep track of dates?
How will Initialize and Update be modified to handle the new file format?
How is the date of the current entry computed?
What changes to the top-level decomposition are necessary?
How is the code tested?

High-Level Design of the History Editor
What commands will be provided in the history file editor?
How is the history file editor organized?
How are the history entries represented?
How do we create an abstract view?
What operations have we left out?
How are the commands and abstract operations related?
Is a file or an array best?

Implementing the Abstract Operations
How is the "find" operation coded?
How is a file searched?
What happens when an entry is found?
How is the "insert" operation coded?
How is the "delete" operation coded?
How is the "print all entries" operation coded?
How is the "print all entries between two given dates" operation coded?
How is the "initialize" operation coded?
What about the code that manipulates dates?
How can the code be tested?
What bugs are encountered?

Designing and Developing the Remainder of the History Editor
What remains to be designed?
What should the "get a command" step do?
What worked before?
How do we select an alternative?
What errors will the program check?
What are the pros and cons of each decomposition?

The "Get the Line, Process the Command Word" Design
What will GetCommand and ExecuteCommand do?
How will GetCommand and ExecuteCommand communicate?
How will ExecuteCommand work?
What are the headers for these functions?
What is a consistent design for the Get functions?
What about illegal commands?
How should the errors be indicated?
How is GetCommand coded?
How does the design of GetCommand affect the main program?
How is ExecuteCommand designed?
How is ExecuteList designed?
How is ExecuteAdd coded?
How are the remaining Execute routines coded?
How is the GetDate function designed?
What are possible errors in a date?
How is GetDate decomposed?
How is all this code tested and debugged?

The "Get a Complete Legal Command" Design
What are the differences between the two designs?
How will the main program change?
How will the Execute functions change?
How will the new GetCommand be organized?
How is the command word analyzed?
How are the arguments analyzed?
Which version is better?

  1. 41
  2. 42

Chapter 8

Durk Jan de Bruin

Programmers' Summary

In this case study, we add three features to the You Are What You Eat program. One is to copy the fat and calorie graphs to a file, allowing the user to print them or refer to them later. Another is the use of a food information file to let the user enter food names and serving sizes rather than actual fat and calorie values. The third provides a way for the user to edit the history file.

We could add all the new features to the original program. We choose instead to invent a separate program to do the editing, hoping to provide a system of programs that is both easier for the user to understand and easier for us to manage.

Even as separate programs, they each contain several hundred lines of code, larger than any of the programs in earlier case studies. Yet implementing the changes to the You Are What You Eat program is straightforward, as is designing and developing the history file editor. Why? There are two main reasons.

First, each program can be decomposed into sections to handle input, processing, and output, along with another section for accessing and updating the history file. We isolate interaction with the user in the input section, access to the history file in history file section, and so on. As a result, it is clear where to make each modification to the original You Are What You Eat program. Each of the sections in both programs can be tested separately, reducing the probability of undetected bugs.

Second, almost all the code results from applying templates from previous case studies. Array searching and processing templates are adapted to search and process the history and food files. (These templates suggest good test data as well.) The "select from alternatives" template forms the backbone of the code to execute commands in the history file editor. Routines to read food names, servings, editor commands, and arguments are patterned on code from Is It Legal?, Space Text, and You Are What You Eat. Code to check dates for legality is copied from The Calendar Shop. This reliance on previously developed code makes the program both easier to design, since we are designing very little code from scratch, and easier to test and debug, since we have already tested the code or something very similar before.

We use multiple debugging switches in the new version of You Are What You Eat so that we can focus on the actions in a particular program section rather than wade through debugging output for all the sections. In the history file editor, we do not have any debugging switch, relying instead on the list commands already being built into the editor. (For this reason, the list command is the first command we test and debug.)

The decision made in You Are What You Eat to store file entries most recent first may have been unwise. It complicates the design of code to search the file and leads to at least one bug.

The main program for the history file editor is an application of the "input one, process one" template, commonly known as a command interpreter. It consists of a loop that first reads, then executes a command. In any command interpreter, there is the question of how much to do in each step. "Reading" a command might be as little as reading its characters into a string or as much as analyzing all the command's components for legality and translating them into an internal form. We design two versions of the code. In one, the "get a line, process the command word" version, the "read a command" step reads a line of input, then classifies the first word as a command; command execution routines isolate the command arguments from the line. In the other, the "get a complete legal command" version, the "read a command" step reads and analyzes the entire command. The choice between the two approaches depends on how similar commands are to one another - the more similar, the better the "get a complete legal command" version - and on what modifications are anticipated for the program.

Several decomposition questions are encountered. At what point should the food name be looked up in the food information file? At what point should the current date be determined? Where should errors be handled? These all are answered to maintain consistency with other parts of the program and to isolate references to program data structures in a small number of routines.

Python constructs introduced in this case study include enumerated types (for commands and error status indicators) and files of dictionary. Members of an enumerated type are used in the same way as integer constants were in The Calendar Shop and Is It Legal? The advantage of an enumerated type is that it can be concisely defined. A file of dictionary allows information to be input without being analyzed. This saves code and execution time. The disadvantages of a file of dictionary are that it can be created only by a program, not by a text editor; its contents can be printed only by a program; and one must ensure that the file-creating program and the file-reading program are using identical definitions for the components of the file.

  1. 43
  2. 44

Chapter 8

Durk Jan de Bruin

Making Sense of You Forgot What You Ate

Analysis

8.35 Describe the advantages of using an array rather than a file for the history entries. What routines would have been designed differently if an array had been used?

Reflection

8.36 Would a replace command have been more confusing than the add and delete commands? Why or why not?

Reflection

8.37 Would the design process have been just as easy if we had started with the "get a complete legal command" version of the program instead of the "get a line, process the command word" version? Why or why not?

Reflection

8.38 Suppose one of your friends volunteered to make the revisions to You Are What You Eat. What would you need to tell your friend about the program in order to orient him or her to the code and design most effectively?

Reflection

Suppose you were working with a programming partner on the revisions. How would you split the task most equitably? What coordination problems would you expect?

Debugging

8.40 Add mutations to the code, and get another programmer to find them. Where in the code are bugs most difficult to detect?

Debugging

8.41 It is possible for the history file editor in the Python Code section to leave fewer than thirty entries in the history file, and the revised You Are What You Eat program will crash as a result. Describe circumstances under which this will happen.

Reflection

The bug described in question 8.41 involved the interaction between two programs. How does testing and debugging the interaction between two programs differ from testing and debugging the interaction between two subprograms?

Modification

8.43 Fix the bug described in question 8.41. In which program should the fix be incorporated?

Modification

8.44 Modify the programs designed in You Forgot What You Ate so that all the user input of fat and calorie information is read in one program, and all the graphing is done in another. Compare this organization to the one we produced.

Application

8.45 Write a program to manipulate a library data base. Each entry in the data base contains information about a book: its title, its lending status, and the date it is to be returned. The program should accept commands that add a book to the data base, delete a book from the data base, signal that the book has been borrowed, signal that the book has been returned, and list all the books currently loaned out.


Linking to Previous Case Studies

Reflection

8.46 In both You Are What You Eat and in You Forgot What You Ate, we encountered a choice between using an array and using a file. Compare the circumstances in the two case studies. Which circumstances were similar and which were different?

Reflection

8.47 How does separating the design of abstract operations for a data type from the design of the code for those operations help isolate code that may later need to be modified?

Reflection

8.48 In what ways are the design of abstract operations for a data type similar to those of postponing details in The Calendar Shop?

Application

8.49 The programs in this case study use code from The Calendar Shop to manipulate dates. Describe some other applications in which code that manipulates dates would be useful, and indicate what other operations on dates would have to be implemented in the applications you describe.

  1. 45
  2. 46

Chapter 8

Durk Jan de Bruin

Revisions to You Are What You Eat

MonthType = ("JANUARY", "FEBRUARY", "MARCH", "APRIL", "MAY", "JUNE", "JULY", "AUGUST", "SEPTEMBER", "OCTOBER", "NOVEMBER", "DECEMBER")
DayType = range(1,32)
YearType = range(1990,2100)
DateType = {
"month": '',
"day": 0,
"year": 0
}
EntryType = {
"date": DateType.copy(),
"fat": 0,
"calories": 0
}
FoodEntryType = {
"foodName": '',
"fat": 0,
"calories": 0
}
HISTORYSIZE = 0
DONESTR = 'done'
BLANK = ' '
MAXLINELEN = 10
MAXGRAPHLINES = 10
MAXFAT = 400
MAXCALORIES = 6000
MAXSERVINGS = 10
DEBUGGING = False
DEBUGHISTORY = False
FILEINPUT = False
DEBUGDATES = False
DEBUGFOOD = False
HistoryType = []
for i in range(0,30):
HistoryType. append(EntryType .copy())
StringType = ''
LineType = {
"length": 0,
"chars": ''
}
historyFile = open("historyFile.txt", "a")
foodinfo = open("foodinfo.txt", "a")
testFile = open("testFile.txt", "a")
entry = EntryType.copy()
recentHistory = HistoryType.copy()
foodEntry = FoodEntryType.copy()

def Empty(line):
Empty = (line == '')
return Empty

def Equal(line, s):
if line == s:
Equal = True
else:
Equal = False
return Equal

""" Return true exactly when year is a leap year."""

def LeapYear(year):
LeapYear = (year % 400 == 0) or ((year % 4 == 0) and (year % 100 != 0))
return LeapYear

""" Return 1 when year is a leap year, 0 otherwise. """

def LeapDay(year):
if LeapYear(year):
LeapDay = 1
else:
LeapDay = 0
return LeapDay

# Return the number of days in the given month.

def NumberOfDaysIn(month, year):
if (month == "JANUARY" or month == "MARCH" or month == "MAY" or month == "JULY" or month == "AUGUST" or month == "OCTOBER" or month == "DECEMBER"):
NumberOfDaysIn = 31
elif month == "FEBRUARY":
NumberOfDaysIn = 28 + LeapDay(year)
else:
NumberOfDaysIn = 30
return NumberOfDaysIn

""" Return next date after the date in current. """

def FindSuccessor(current, next):
current = current.split("-")
current[0] = int(current[0])
current11 = current[1]
current[2] = int(current[2])
if current[1] == "JANUARY":
current[1] = 1
if current[1] == "FEBRUARY":
current[1] = 2
if current[1] == "MARCH":
current[1] = 3
if current[1] == "APRIL":
current[1] = 4
if current[1] == "MAY":
current[1] = 5
if current[1] == "JUNE":
current[1] = 6
if current[1] == "JULY":
current[1] = 7
if current[1] == "AUGUST":
current[1] = 8
if current[1] == "SEPTEMBER":
current[1] = 9
if current[1] == "OCTOBER":
current[1] = 10
if current[1] == "NOVEMBER":
current[1] = 11
if current[1] == "DECEMBER":
current[1] = 12
if current[0] < NumberOfDaysIn (current11, current[2]):
next["month"] = current11
next["day"] = current[0] + 1
next["year"] = current[2]
elif current[1] < 12:
next["month"] = current[1]+1
next["day"] = 1
next["year"] = current[2]
else:
next["month"] = "JANUARY"
next["day"] = 1
next["year"] = current[2] + 1
if DEBUGDATES:
print("{} {} {}".format(next["day"], next["month"], next["year"]))
return next

  1. 47
  2. 48

Chapter 8

Durk Jan de Bruin

""" Return true exactly when numServings constitutes a reasonable serving size. """

def IsInServingRange (numServings):
IsInServingRange = (numServings >= 1) and (numServings <= MAXSERVINGS)
return IsInServingRange

""" line contains a food name. Search for it in foodinfo. If it's there, return true in found and return the corresponding food entry in foodEntry; otherwise return false in found. """

def Search(line, foodEntry, found):
found = False
foodinfo = open('foodinfo.txt', 'r')
for line1 in foodinfo:
line2 = line1.split()
found = Equal(line2[0], line)
if found == True:
foodEntry ["foodName"] = line2[0]
foodEntry ["fat"] = int(line2[1])
foodEntry ["calories"] = int(line2[2])
break
if DEBUGFOOD:
print('*** checking ', foodEntry ["foodName"])
return found

""" Ask the user for a food name, and keep prompting until a legal food name is provided. The user may either supply a food name, in which case the corresponding entry from foodinfo is returned in foodEntry and false is returned in done, or the word "done", in which case true is returned in done. """

def ReadFood(inFile, foodEntry, done):
found = False
error = False
line = LineType.copy()
done = False
while not done:
error = False
print('Please type a food name')
input1 = input()
if Empty(input1):
error = True
else:
found = Search(input1, foodEntry, found)
if not found:
error = True
if found:
break
if error:
print('That food is not in the dictionary of foods.')

""" Ask the user for a number of servings, and keep prompting until a legal number is provided. foodEntry contains information about the serving units that is printed in the prompt. Return the number in numServings."""

def ReadServings(inFile, foodEntry, numServings):
error = False
line = LineType.copy()
done = False
while not done:
error = False
print('How many servings? One serving = {}
calories'. format(foodEntry ["calories"]))
input1 = input()
if Empty(input1):
error = True
elif not input1.isnumeric():
error = True
else:
numServings = int(input1)
if IsInServingRange (numServings):
break
if not IsInServingRange (numServings):
error = True
if error:
print('You must provide an integer number of servings no more than', MAXSERVINGS)
return numServings

""" Read today's food entry from the user, returning it in entry."""
def readlndata():
global HISTORYSIZE
k = 0
myFile = open('historyFile.txt', 'r')
for line in myFile:
line2 = line.split()
date = line2[0]
fat = line2[1]
calories = line2[2]
fat1 = int(fat)
calories1 = int(calories)
recentHistory[k]["date"] = date
recentHistory[k]["fat"] = fat1
recentHistory[k]["calories"] = calories1
k = k + 1
HISTORYSIZE = k

def ReadEntry(inFile, today):
numServings = 0
done = False
foodEntry = FoodEntryType.copy()
entry["date"] = today
entry["fat"] = 0
entry["calories"] = 0
while not done:
ReadFood(inFile, foodEntry, done)
numServings = ReadServings(inFile, foodEntry, numServings)
entry["fat"] = entry["fat"] + numServings * foodEntry["fat"]
entry["calories"] = entry["calories"] + numServings * foodEntry["calories"]
print("Type the word \"done\"--without the quotes--if you\'re finished.")
print("Or press \"ENTER\" and continue.")
input2 = input()
if input2 == DONESTR:
done = True
if DEBUGFOOD:
print('*** accumulated fat = {} accumulated calories = {}'.format(entry["fat"], entry["calories"]))
f = open("historyFile.txt", "a")
f.write("{}-{}-{} {} {}\n".format(entry["date"]["day"], entry["date"]["month"], entry["date"]["year"], entry["fat"], entry["calories"]))
f.close()
readlndata()

  1. 49
  2. 50

Chapter 8

Durk Jan de Bruin

def Initialize(historyFile, recentHistory):
numEntries = 0
dayNum = 0
entry = EntryType.copy()
today = DateType.copy()
readlndata()
for dayNum in range(HISTORYSIZE):
if DEBUGHISTORY:
print('*** from history: ')
print(recentHistory [dayNum]["date"])
print( ' fat = {} calories = {}'.format(recentHistory [dayNum]["fat"], recentHistory[dayNum]["calories"]))
next_date = FindSuccessor (recentHistory [HISTORYSIZE-1]["date"], today)
print('The food data you are about to enter is assumed to be for ', end = '')
print("{} {} {}".format(next_date["day"], next_date["month"], next_date["year"]))
print('Quit this program and run the history file editor, if this is incorrect.')
if FILEINPUT:
ReadEntry(testEile, today)
else:
ReadEntry(historyFile, today)

def FatAverage (recentHistory):
sum1 = 0
dayNum = 0
for dayNum in range(0,HISTORYSIZE):
sum1 = sum1 + recentHistory[dayNum]["fat"]
FatAverage = sum1 / HISTORYSIZE
return FatAverage

def CalorieAverage (recentHistory):
sum1 = 0
dayNum = 0
for dayNum in range(0,HISTORYSIZE):
sum1 = sum1 + recentHistory[dayNum]["calories"]
CalorieAverage = sum1 / HISTORYSIZE
return CalorieAverage

def PrintAverages (recentHistory):
print('\nOver the last ' + str(HISTORYSIZE) +' days')
print('The average fat consumption has been ' + str(FatAverage (recentHistory)) + ' grams, and ')
print('The average calorie consumption has been ' + str(CalorieAverage (recentHistory)) + ' calories.')
print("\n")

def PrintFatGraph (recentHistory):
leftValue = 0
rightValue = 0
dayNum = 0
lineNum= 0
print('Fat intake in grams:')
rightValue = MAXFAT
leftValue = rightValue - MAXFAT // MAXGRAPHLINES + 1;
for lineNum in range(MAXGRAPHLINES):
print(" {1:3d} - {0:3d}|".format(leftValue, rightValue),end='')
for dayNum in range(HISTORYSIZE):
if (recentHistory[dayNum]["fat"] >= leftValue) and (recentHistory[dayNum]["fat"] <= rightValue):
print(' X',end='')
else:
print(' ',end = '')
print('')
rightValue = leftValue - 1
leftValue = leftValue - MAXFAT // MAXGRAPHLINES
print(' ' + '+',end='')
for dayNum in range(31):
print('---',end='')
print('\n')
print(' ',end='')
for dayNum in range(31):
if dayNum % 5 == 0:
print(" {}".format(dayNum),end='')
else:
print(" {}".format(BLANK),end='')
print('\n')
print(" {}".format(BLANK) + 'Number of days')

def PrintCalorieGraph (recentHistory):
leftValue = 0
rightValue = 0
dayNum = 0
lineNum= 0
print('Calorie intake:')
rightValue = MAXCALORIES
leftValue = rightValue - MAXCALORIES // MAXGRAPHLINES + 1;
for lineNum in range(MAXGRAPHLINES):
print(" {1:4d} - {0:4d}|".format(leftValue, rightValue),end='')
for dayNum in range(HISTORYSIZE):
if (recentHistory[dayNum]["calories"] >= leftValue) and (recentHistory[dayNum]["calories"] <= rightValue):
print(' X',end='')
else:
print(' ',end = '')
print('')
rightValue = leftValue - 1
leftValue = leftValue - MAXCALORIES // MAXGRAPHLINES
print(' ' + '+',end='')
for dayNum in range(31):
print('---',end='')
print('\n')
print(' ',end='')
for dayNum in range(31):
if dayNum % 5 == 0:
print(" {}".format(dayNum),end='')
else:
print(" {}".format(BLANK),end='')
print('\n')
print(" {}".format(BLANK) + 'Number of days')

def PrintGraphs (recentHistory):
PrintFatGraph (recentHistory)
print("\n")
print("\n")
PrintCalorieGraph (recentHistory)

  1. 51
  2. 52

Chapter 8

Durk Jan de Bruin

def Update(historyFile, recentHistory):
if DEBUGGING:
f = open("testFile.txt", "w")
f.write('Over the last ' + str(HISTORYSIZE) +' days\n')
f.write("Fat Calories\n")
for dayNum in range(HISTORYSIZE):
f.write("{} {}\n".format (recentHistory[dayNum]["fat"], recentHistory [dayNum]["calories"]))
f.close()

# Main Program

if DEBUGGING:
ReadEntry(testFile, entry)
else:
Initialize(historyFile, recentHistory)
PrintAverages (recentHistory)
PrintGraphs (recentHistory)


Implementation of the Abstract History File Operations

""" Print the information in the given entry."""

def PrintEntry(entry):
print('Date = ', end = '')
print(entry["date"])
print('Fat = {} , calories = {}'.format(entry["fat"], entry["calories"]))

""" Read the information for a food entry."""

def ReadEntry(entry):
(The code for ReadEntry appears in the previous section)

""" Copy remaining elements from original to temp, then copy all elements in temp back to original."""

def CopyThenCopyBack(original, temp):
entry1 = EntryType.copy()
original = open("historyFile.txt", "r")
lines = original.readlines()
original.close()
temp = open("temp.txt", "w+")
for line in lines:
temp.write(line)
temp.close()
temp = open("temp.txt", "r")
lines = temp.readlines()
temp.close()
original = open("historyFile.txt", "w+")
for line in lines:
original.write(line)
original.close()

""" Search for an entry with the given date in history, while copying entries from history to temp. Read only as far as necessary in history. If an entry with the given date is found, return it in entry and return true in found; otherwise return false in found, temp will represent the position either of the entry found or the entry with the next earlier date if the search is unsuccessful."""

def Find(history, date, found, entry, temp):
foundAtOrBefore = False
found = False
numdates = 0
position = 0
myFile = open('historyFile.txt', 'r')
for lines2 in myFile:
numdates = numdates + 1
for i in range(numdates):
if Same(history[i]["date"], date):
foundAtOrBefore = True
found = True
position = i
break
return found, position

""" Insert the given entry at the given position (temp) in history."""

def Insert(history, entry, temp):
original = open("historyFile.txt", "w+")
for line in entry:
original.write(line)
original.close()
CopyThenCopyBack (history, temp)

""" Delete the entry at the given position (temp) in history."""

def Delete(history, temp):
CopyThenCopyBack (history, temp)

""" Print all entries in history."""

def PrintAll(history):
entry1 = EntryType.copy()
historyFile1 = open('historyFile.txt', 'r')
for line1 in historyFile1:
line2 = line1.split()
date = line2[0]
fat = line2[1]
calories = line2[2]
fat1 = int(fat)
calories1 = int(calories)
entry1["date"] = date
entry1["fat"] = fat1
entry1["calories"] = calories1
PrintEntry(entry1)

""" Print all entries in history up to and including the given date."""

def PrintUpTo(history, date, temp):
done = False
entry = EntryType.copy()
while not done:
if AtOrBefore(date, entry["date"]):
PrintEntry(entry)
else:
done = True

  1. 53
  2. 54

Chapter 8

Durk Jan de Bruin

""" Initialize history."""

def Initialize(history):
myFile = open('historyFile.txt', 'r')
k = 0
for line in myFile:
line2 = line.split()
date = line2[0]
fat = line2[1]
calories = line2[2]
fat1 = int(fat)
calories1 = int(calories)
date1 = date.split("-")
history[k]["date"] = list((int(date1[0]), date1[1], int(date1[2])))
history[k]["date"][1] = MonthEquivalent(date1[1])
history[k]["fat"] = fat1
history[k]["calories"] = calories1
k = k + 1


The Rest of the History Editor

BLANK = ' '
DATESEPARATOR = '-'
MAXLINELEN = 80
MAXWORDLEN = 20
LISTWORD = 'list'
ADDWORD = 'add'
DELETEWORD = 'delete'
HELPWORD = 'help'
QUITWORD = 'quit'
BLANKWORD = ' '
DEBUGFOOD = False
MAXSERVINGS = 10
DONESTR = 'done'
LineType = {
"chars": '',
"length": 0,
"position": 0
}
WordType = ''
CommandType = ["LISTCMD", "ADDCMD", "DELETECMD", "HELPCMD", "QUITCMD", "UNKNOWN"]
StatusType = ["SUCCESS", "NOINPUT", "LINETOOLONG", "NOWORD", "WORDTOOLONG", "NOCOMMAND", "BADCOMMAND", "NODATE", "TOOMANYDATES", "BADNUM", "BADSEP", "EXTRAJUNK", "BADDATE", "ALREADYTHERE", "NOTTHERE", "EMPTYRANGE"]
MonthType = ["JANUARY", "FEBRUARY", "MARCH", "APRIL", "MAY", "JUNE", "JULY", "AUGUST", "SEPTEMBER", "OCTOBER", "NOVEMBER", "DECEMBER"]
DayType = range(1,32)
YearType = range(1990,2100)
DateType = {
"month": '',
"day": 0,
"year": 0
}
EntryType = {
"date": DateType.copy(),
"fat": 0,
"calories": 0
}
FoodEntryType = {
"foodName": '',
"fat": 0,
"calories": 0
}
SequenceType = EntryType.copy()
PositionType = EntryType.copy()
getDateCount = 0
history = []
for i in range(0,30):
history.append (EntryType.copy())
done = False
line = LineType.copy()
command = ''
status = ''
entry = EntryType.copy()

""" Get the next word from line and return it in word. Update the line position. Return the result of the word scanning in status: success, no words left in line, or too long a word. """

def GetWord(line):
global status
words = line["chars"].split()
if len(words[0]) == 0:
status = "NOWORD"
if len(words[0]) > MAXWORDLEN:
status = "WORDTOOLONG"
return words[0]

""" Get a command word from the line, and return it (translated to the corresponding CommandType member) in command. If the next word in line does not represent a legal command, indicate the fact in status. """

def GetCmdWord(line):
global command, status
status = "SUCCESS"
command = "UNKNOWN"
word = GetWord(line)
if status == "NOWORD":
status = "NOCOMMAND"
elif word == LISTWORD:
command = "LISTCMD"
elif word == ADDWORD:
command = "ADDCMD"
elif word == DELETEWORD:
command = "DELETECMD"
elif word == HELPWORD:
command = "HELPCMD"
elif word == QUITWORD:
command = "QUITCMD"
else:
status = "BADCOMMAND"

""" Get a legal date from line, and return it in date. If the next word in line does not represent a legal date, indicate the fact in status. """

def GetDate(line, date):
global status
global getDateCount
word = ''
month = 0
day = 0
year = 0
words = line["chars"].split()
if len(words)>2:
words.pop(0)
for lines in words:
return lines

  1. 55
  2. 56

Chapter 8

Durk Jan de Bruin

def PrintEntry(entry):
print('Date = ', end = '')
print(entry["date"])
print('Fat = {} , calories = {}'.format(entry["fat"], entry["calories"]))
print("")

def PrintEntry1(entry):
print('Date = ', end = '')
print("{}-{}-{}".format(entry["date"][0], entry["date"][1], entry["date"][2]))
print('Fat = {} , calories = {}'.format(entry["fat"], entry["calories"]))
print("")

""" Print all entries in history."""

def PrintAll(history):
entry1 = EntryType.copy()
historyFile1 = open('historyFile.txt', 'r')
for line1 in historyFile1:
line2 = line1.split()
date = line2[0]
fat = line2[1]
calories = line2[2]
fat1 = int(fat)
calories1 = int(calories)
entry1["date"] = date
entry1["fat"] = fat1
entry1["calories"] = calories1
PrintEntry(entry1)

""" Return true exactly when year is a leap year."""

def LeapYear(year):
LeapYear = (year % 400 == 0) or ((year % 4 == 0) and (year % 100 != 0))
return LeapYear

""" Return 1 when year is a leap year, 0 otherwise. """

def LeapDay(year):
if LeapYear(year):
LeapDay = 1
else:
LeapDay = 0
return LeapDay

# Return the number of days in the given month.

def NumberOfDaysIn(month, year):
if (month == 1 or month == 3 or month == 5 or month == 7 or month == 8 or month == 10 or month == 12):
NumberOfDaysIn = 31
elif month == 2:
NumberOfDaysIn = 28 + LeapDay(year)
else:
NumberOfDaysIn = 30
return NumberOfDaysIn

""" Return true exactly when month, day, and year collectively represent a legal date."""

def LegalDate(month, day, year):
if not (month in range(1, 13)):
LegalDate = False
else:
LegalDate = day in range(1, NumberOfDaysIn(month, year) + 1)
return LegalDate

""" Return true exactly when datel precedes date2. """

def Precedes(date1, date2):
if date1["year"] < date2["year"]:
Precedes = True
elif date1["year"] > date2["year"]:
Precedes = False
elif date1["month"] < date2["month"]:
Precedes = True
elif date1["month"] > date2["month"]:
Precedes = False
else:
Precedes = date1["day"] < date2["day"]
return Precedes

""" Return true exactly when datel and date2 are identical. """

def Same(date1, date2):
Same = (date1[1] == date2["month"]) and (date1[0] == date2["day"]) and (date1[2] == date2["year"])
return Same

""" Return true when date1 comes at or before date2. """

def AtOrBefore(date1, date2):
AtOrBefore = Same(date1, date2) or Precedes(date1, date2)
return AtOrBefore

""" Search for an entry with the given date in history, while copying entries from history to temp. Read only as far as necessary in history. If an entry with the given date is found, return it in entry and return true in found; otherwise return false in found, temp will represent the position either of the entry found or the entry with the next
earlier date if the search is unsuccessful."""

def Find(history, date, found, entry, temp):
foundAtOrBefore = False
found = False
numdates = 0
position = 0
myFile = open('historyFile.txt', 'r')
for lines2 in myFile:
numdates = numdates + 1
for i in range(numdates):
if Same(history[i]["date"], date):
foundAtOrBefore = True
found = True
position = i
break
return found, position

def Empty(line):
Empty = (line == '')
return Empty

def Equal(line, s):
if line == s:
Equal = True
else:
Equal = False
return Equal

""" Return true exactly when numServings constitutes a reasonable serving size. """

def IsInServingRange (numServings):
IsInServingRange = (numServings >= 1) and (numServings <= MAXSERVINGS)
return IsInServingRange

  1. 57
  2. 58

Chapter 8

Durk Jan de Bruin

""" line contains a food name. Search for it in foodinfo. If it's there, return true in found and return the corresponding food entry in foodEntry; otherwise return false in found. """

def Search(line, foodEntry, found):
found = False
foodinfo = open('foodinfo.txt', 'r')
for line1 in foodinfo:
line2 = line1.split()
found = Equal(line2[0], line)
if found == True:
foodEntry ["foodName"] = line2[0]
foodEntry ["fat"] = int(line2[1])
foodEntry ["calories"] = int(line2[2])
break
if DEBUGFOOD:
print('*** checking ', foodEntry["foodName"])
return found

""" Ask the user for a food name, and keep prompting until a legal food name is provided. The user may either supply a food name, in which case the corresponding entry from foodinfo is returned in foodEntry and false is returned in done, or the word "done", in which case true is returned in done. """

def ReadFood(inFile, foodEntry, done):
found = False
error = False
line = LineType.copy()
done = False
while not done:
error = False
print('Please type a food name')
input1 = input()
if Empty(input1):
error = True
else:
found = Search(input1, foodEntry, found)
if not found:
error = True
if found:
break
if error:
print('That food is not in the dictionary of foods.')

""" Ask the user for a number of servings, and keep prompting until a legal number is provided. foodEntry contains information about the serving units that is printed in the prompt. Return the number in numServings."""

def ReadServings(inFile, foodEntry, numServings):
error = False
line = LineType.copy()
done = False
while not done:
error = False
print('How many servings? One serving = {} calories'
.format(foodEntry ["calories"]))
input1 = input()
if Empty(input1):
error = True
elif not input1.isnumeric():
error = True
else:
numServings = int(input1)
if IsInServingRange (numServings):
break
if not IsInServingRange (numServings):
error = True
if error:
print('You must provide an integer number of servings no more than', MAXSERVINGS)
return numServings

def ReadEntry(inFile, today):
numServings = 0
done = False
foodEntry = FoodEntryType.copy()
entry["date"] = today
entry["fat"] = 0
entry["calories"] = 0

while not done:
ReadFood(inFile, foodEntry, done)
numServings = ReadServings(inFile, foodEntry, numServings)
entry["fat"] = entry["fat"] + numServings * foodEntry["fat"]
entry["calories"] = entry["calories"] + numServings * foodEntry["calories"]
print("Type the word \"done\"--without the quotes--if you\'re finished.")
print("Or press \"ENTER\" and continue.")
input2 = input()
if input2 == DONESTR:
done = True
if DEBUGFOOD:
print('*** accumulated fat = {} accumulated calories = {}'.format(entry["fat"], entry["calories"]))
f = open("historyFile.txt", "a")
f.write("{}-{}-{} {} {}\n".format(entry["date"]["day"], entry["date"]["month"], entry["date"]["year"], entry["fat"], entry["calories"]))
f.close()

def Initialize(history):
myFile = open('historyFile.txt', 'r')
k = 0
for line in myFile:
line2 = line.split()
date = line2[0]
fat = line2[1]
calories = line2[2]
fat1 = int(fat)
calories1 = int(calories)
date1 = date.split("-")
history[k]["date"] = list((int(date1[0]), date1[1], int(date1[2])))
history[k]["date"][1] = MonthEquivalent(date1[1])
history[k]["fat"] = fat1
history[k]["calories"] = calories1
k = k + 1

  1. 59
  2. 60

Chapter 8

Durk Jan de Bruin

def MonthEquivalent(month):
if month == "JANUARY":
return 1
if month == "FEBRUARY":
return 2
if month == "MARCH":
return 3
if month == "APRIL":
return 4
if month == "MAY":
return 5
if month == "JUNE":
return 6
if month == "JULY":
return 7
if month == "AUGUST":
return 8
if month == "SEPTEMBER":
return 9
if month == "OCTOBER":
return 10
if month == "NOVEMBER":
return 11
if month == "DECEMBER":
return 12

def MonthEquivalent1(month):
if month == 1:
return "JANUARY"
if month == 2:
return "FEBRUARY"
if month == 3:
return "MARCH"
if month == 4:
return "APRIL"
if month == 5:
return "MAY"
if month == 6:
return "JUNE"
if month == 7:
return "JULY"
if month == 8:
return "AUGUST"
if month == 9:
return "SEPTEMBER"
if month == 10:
return "OCTOBER"
if month == 11:
return "NOVEMBER"
if month == 12:
return "DECEMBER"

def ExecuteList(line, history):
global status
dates = []
for i in range(2):
dates.append (DateType.copy())
tempDate = DateType.copy()
numDates = 0
done = False
found = False
position = 0
k = 0
entry = EntryType.copy()
words = line["chars"].split()
words.pop(0)
numDates = len(words)
if numDates > 2:
status = "TOOMANYDATES"
done = True
else:
for dates1 in words:
date1 = dates1.split("-")
dates[k]["day"] = int(date1[0])
dates[k]["month"] = int(date1[1])
dates[k]["year"] = int(date1[2])
k = k + 1
for dates1 in dates:
if not LegalDate(dates1["month"], dates1["day"],
dates1["year"]):
status = "BADNUM"
if numDates == 0:
PrintAll(history)
status = "SUCCESS"
if numDates == 1:
found, position = Find(history, dates[0], found,
entry, position)
if found:
PrintEntry1 (history[position])
status = "SUCCESS"
else:
status = "NOTTHERE"
if numDates == 2:
numdates = 0
found1, position1 = Find(history, dates[0],
found, entry, position)
found2, position2 = Find(history, dates[1],
found, entry, position)
if found1 and found2:
PrintEntry1 (history[position1])
myFile = open('historyFile.txt', 'r')
for lines2 in myFile:
numdates = numdates + 1
for i in range(numdates):
if history[i]["date"][0] ==
dates[0]["day"] and history[i] ["date"][1]
== dates[0]["month"] and history[i]
["date"][2] == dates[0]["year"]:
continue
if history[i]["date"][0] == dates[1]
["day"] and history[i]["date"][1]
== dates[1]["month"] and history[i]["date"]
[2] == dates[1]["year"]:
continue
if dates[0]["year"] < history[i]["date"][2]
< dates[1]["year"]:
PrintEntry1 (history[i])
elif history[i]["date"][2] == dates[1]
["year"]:
if dates[0]["month"] < history[i]
["date"][1] < dates[1]["month"]:
PrintEntry1 (history[i])
elif history[i]["date"][1] ==
dates[0]["month"] or history[i]
["date"][1] == dates[1]["month"]:
if dates[0]["day"] > dates[1]["day"]:
if dates[0]["day"]< history[i]
["date"][0] or history[i]["date"]
[0] < dates[1]["day"]:
PrintEntry1 (history[i])
elif dates[0]["day"] < dates[1]["day"]:
if dates[0]["day"]<= history[i]
["date"][0] < dates[1]["day"]:
PrintEntry1 (history[i])
PrintEntry1 (history[position2])

  1. 61
  2. 62

Chapter 8

Durk Jan de Bruin

def ExecuteAdd(line, history):
global status
date = DateType.copy()
tempDate = DateType.copy()
entry = EntryType.copy()
dates = []
for i in range(2):
dates.append (DateType.copy())
found = False
done = False
k = 0
position = 0
inFile = ''
words = line["chars"].split()
words.pop(0)
numDates = len(words)
if numDates > 2:
status = "TOOMANYDATES"
done = True
else:
for dates1 in words:
date1 = dates1.split("-")
dates[k]["day"] = int(date1[0])
dates[k]["month"] = int(date1[1])
dates[k]["year"] = int(date1[2])
k = k + 1
if not LegalDate(dates[0]["month"], dates[0]
["day"], dates[0]["year"]):
status = "BADNUM"
found, position = Find(history, dates[0],
found, entry, position)
if status!= "BADNUM":
if not found:
dates[0]["month"] = MonthEquivalent1
(dates[0]["month"])
ReadEntry(inFile, dates[0])
dates[0]["month"] = MonthEquivalent
(dates[0]["month"])
Initialize (history)
else:
status = "ALREADYTHERE"
def ExecuteDelete(line, history):
global status
date = DateType.copy()
tempDate = DateType.copy()
entry = EntryType.copy()
dates = []
for i in range(2):
dates.append (DateType.copy())
found = False
done = False
k = 0
position = 0
inFile = ''
words = line["chars"].split()
words.pop(0)
numDates = len(words)
if numDates > 2:
status = "TOOMANYDATES"
done = True
else:
for dates1 in words:
date1 = dates1.split("-")
dates[k]["day"] = int(date1[0])
dates[k]["month"] = int(date1[1])
dates[k]["year"] = int(date1[2])
k = k + 1
if not LegalDate (dates[0]["month"], dates[0]["day"], dates[0]["year"]):
status = "BADNUM"
found, position = Find(history, dates[0], found, entry, position)
if status!= "BADNUM":
if not found:
status = "NOTTHERE"
else:
a_file = open("historyFile.txt", "r")
lines = a_file.readlines()
a_file.close()
del lines[position]
new_file = open("historyFile.txt", "w+")
for line in lines:
new_file. write(line)
new_file.close()
Initialize(history)

def ExecuteHelp(line, history):
print('\nCommand formats appear below. Where the notation ')
print('appears, you\'re supposed to supply a legal date, ')
print('for instance, 24/8/2020.')
print('To get this message:')
print('help')
print('To leave the history editor:')
print('quit')
print('To list the information for all history entries:')
print('list')
print('To list the information for the entry for a given date:')
print('list ')
print('To list the information for all entries between two dates:')
print('list ')
print('To add an entry for a given date:')
print('add ')
print('To delete the entry for a given date:')
print('delete ')
print("\n")

  1. 63
  2. 64

Chapter 8

Durk Jan de Bruin

""" The user has typed a quit command. """

def ExecuteQuit(line, history):
print('Leaving the history editor.')

""" Execute the given command. Its remaining arguments are still on line."""

def ExecuteCommand(line, history):
global command, status
if command == "LISTCMD":
ExecuteList(line, history)
if command == "ADDCMD":
ExecuteAdd(line, history)
if command == "DELETECMD":
ExecuteDelete(line, history)
if command == "HELPCMD":
ExecuteHelp(line, history)
if command == "QUITCMD":
ExecuteQuit(line, history)

def PrintErrorMsg(status):
if status == "SUCCESS":
print('*** INTERNAL ERROR IN PROGRAM ***')
if status == "NOINPUT":
print('*** Input ran out unexpectedly. ***')
if status == "LINETOOLONG":
print('*** There is too much text on the command line. ***')
if status == "NOWORD":
print('*** INTERNAL ERROR IN PROGRAM ***')
if status == "WORDTOOLONG":
print('*** A word on the command line is too long. ***')
if status == "NOCOMMAND":
print('*** There is nothing on the command line. ***')
if status == "BADCOMMAND":
print('*** The first word on the line is not a legal command. ***')
if status == "NODATE":
print('*** INTERNAL ERROR IN PROGRAM ***')
if status == "TOOMANYDATES":
print('*** Too many dates were specified for this command. ***')
if status == "BADNUM":
print('*** The month, day, or year is illegal. ***')
if status == "BADSEP":
print('*** The month and day must, be separated by a slash, as must the day and year. ***')
if status == "EXTRAJUNK":
print('*** Extra text appears after the year. ***')
if status == "BADDATE":
print('*** The number of days in the month is incorrect. ***')
if status == "ALREADYTHERE":
print('*** There is already an entry for that date. ***')
if status == "NOTTHERE":
print('*** There is no entry for that date. ***')
if status == "EMPTYRANGE":
print('*** There are no entries between the two given dates. ***')

""" Read a line from the user. Possible values to return in status are success, no lines to read (user indicated end-of-file), or line too long to store."""

def GetLine(line):
global status
status = "SUCCESS"
line["chars"] = input()
line["length"] = len(line["chars"])
line["position"] = 1
if line["chars"] == '':
status = "NOCOMMAND"
if line["length"] > MAXLINELEN:
status = "LINETOOLONG"
line["chars"] = ''

""" Prompt the user for a command, and return what is typed in command and the result of analyzing its first word in status. """

def GetCommand(line):
global command,status
print('COMMAND? ')
GetLine(line)
if status == "NOINPUT":
command = "QUITCMD"
status = "SUCCESS"
elif status == "SUCCESS":
GetCmdWord(line)

# *** main program ***
Initialize(history)
done = False
while command!= "QUITCMD":
GetCommand(line)
if status == "SUCCESS":
ExecuteCommand(line, history)
if status != "SUCCESS":
PrintErrorMsg(status)


Revisions for the "Get a Complete Legal Command" Approach

BLANK = ' '
DATESEPARATOR = '-'
MAXLINELEN = 80
MAXWORDLEN = 20
LISTWORD = 'list'
ADDWORD = 'add'
DELETEWORD = 'delete'
HELPWORD = 'help'
QUITWORD = 'quit'
BLANKWORD = ' '
DEBUGFOOD = False
MAXSERVINGS = 10
DONESTR = 'done'
MAXCMDLEN = 3

  1. 65
  2. 66

Chapter 8

Durk Jan de Bruin

LineType = {
"chars": '',
"length": 0,
"position": 0
}
WordType = ''
CmdWordType = ["LISTCMD", "ADDCMD", "DELETECMD", "HELPCMD", "QUITCMD", "UNKNOWN"]
StatusType = ["SUCCESS", "NOINPUT", "LINETOOLONG", "NOWORD", "WORDTOOLONG", "NOCOMMAND", "BADCOMMAND", "NODATE",
"TOOMANYDATES", "BADNUM", "BADSEP", "EXTRAJUNK", "BADDATE", "ALREADYTHERE", "NOTTHERE", "EMPTYRANGE"]
MonthType = ["JANUARY", "FEBRUARY", "MARCH", "APRIL", "MAY", "JUNE", "JULY", "AUGUST", "SEPTEMBER", "OCTOBER", "NOVEMBER", "DECEMBER"]
DayType = range(1,32)
YearType = range(1990,2100)
DateType = {
"month": '',
"day": 0,
"year": 0
}
EntryType = {
"date": DateType.copy(),
"fat": 0,
"calories": 0
}
FoodEntryType = {
"foodName": '',
"fat": 0,
"calories": 0
}
WordArrayType = {
"length": 0,
"words": ['','','']
}
CommandType = {
"cmdWord": '',
"numArgs": 0,
"args": [DateType.copy(), DateType.copy()]
}
SequenceType = EntryType.copy()
PositionType = EntryType.copy()
getDateCount = 0
history = []
for i in range(0,30):
history.append (EntryType.copy())
done = False
line = LineType.copy()
command = CommandType.copy()
words = WordArrayType.copy()
status = ''
entry = EntryType.copy()

""" Get the next word from line and return it in word. Update the line position. Return the result of the word scanning in status: success, no words left in line, or too long a word. """

def GetWord(line):
global status
words = line["chars"].split()
if len(words[0]) == 0:
status = "NOWORD"
if len(words[0]) > MAXWORDLEN:
status = "WORDTOOLONG"
return words[0]

""" Convert the first word in words to a command. Return the result in command, and indicate success or any irregularities in status. """
def ConvertToCommand(words):
global status
status = "SUCCESS"
command["cmdWord"] = "UNKNOWN"
if words["length"] == 0:
status = "NOCOMMAND"
elif words["words"][0] == LISTWORD:
command["cmdWord"] = "LISTCMD"
command["numArgs"] = words["length"] - 1
elif words["words"][0] == ADDWORD:
command["cmdWord"] = "ADDCMD"
if words["length"] < 2:
status = "NODATE"
elif words["length"] > 2:
status = "TOOMANYDATES"
else:
command ["numArgs"] = 1
elif words["words"][0] == DELETEWORD:
command["cmdWord"] = "DELETECMD"
if words["length"] < 2:
status = "NODATE"
elif words["length"] > 2:
status = "TOOMANYDATES"
else:
command ["numArgs"] = 1
elif words["words"][0] == HELPWORD:
command["cmdWord"] = "HELPCMD"
command["numArgs"] = 0
elif words["words"][0] == QUITWORD:
command["cmdWord"] = "QUITCMD"
command["numArgs"] = 0
else:
status = "BADCOMMAND"
""" Prompt the user for a command, and return what is typed in command and the result of analyzing its first word in status. """

def GetCommand(line):
global status
print('COMMAND? ')
GetLine(line)
if status == "NOINPUT":
command["cmdWord"] = "QUITCMD"
command["numArgs"] = 0
status = "SUCCESS"
if status == "SUCCESS":
GetWords(line)
if status == "SUCCESS":
ConvertToCommand (words)

  1. 67
  2. 68

Chapter 8

Durk Jan de Bruin

def ExecuteList(line, history):
global status
dates = []
for i in range(2):
dates.append (DateType.copy())
tempDate = DateType.copy()
numDates = 0
done = False
found = False
position = 0
k = 0
entry = EntryType.copy()
words = line["chars"].split()
words.pop(0)
numDates = len(words)
if numDates > 2:
status = "TOOMANYDATES"
done = True
else:
for dates1 in words:
date1 = dates1.split("-")
dates[k]["day"] = int(date1[0])
dates[k]["month"] = int(date1[1])
dates[k]["year"] = int(date1[2])
k = k + 1
for dates1 in dates:
if not LegalDate(dates1["month"], dates1["day"],
dates1["year"]):
status = "BADNUM"
if numDates == 0:
PrintAll(history)
status = "SUCCESS"
if numDates == 1:
found, position = Find(history, dates[0], found,
entry, position)
if found:
PrintEntry1 (history[position])
status = "SUCCESS"
else:
status = "NOTTHERE"
if numDates == 2:
numdates = 0
found1, position1 = Find(history, dates[0],
found, entry, position)
found2, position2 = Find(history, dates[1],
found, entry, position)
if found1 and found2:
PrintEntry1 (history[position1])
myFile = open('historyFile.txt', 'r')
for lines2 in myFile:
numdates = numdates + 1
for i in range(numdates):
if history[i]["date"][0] ==
dates[0]["day"] and history[i] ["date"][1]
== dates[0]["month"] and history[i]
["date"][2] == dates[0]["year"]:
continue
if history[i]["date"][0] == dates[1]
["day"] and history[i]["date"][1]
== dates[1]["month"] and history[i]["date"]
[2] == dates[1]["year"]:
continue
if dates[0]["year"] < history[i]["date"][2]
< dates[1]["year"]:
PrintEntry1 (history[i])
elif history[i]["date"][2] == dates[1]
["year"]:
if dates[0]["month"] < history[i]
["date"][1] < dates[1]["month"]:
PrintEntry1 (history[i])
elif history[i]["date"][1] ==
dates[0]["month"] or history[i]
["date"][1] == dates[1]["month"]:
if dates[0]["day"] > dates[1]["day"]:
if dates[0]["day"]< history[i]
["date"][0] or history[i]["date"]
[0] < dates[1]["day"]:
PrintEntry1 (history[i])
elif dates[0]["day"] < dates[1]["day"]:
if dates[0]["day"]<= history[i]
["date"][0] < dates[1]["day"]:
PrintEntry1 (history[i])
PrintEntry1 (history[position2])

def ExecuteAdd(line, history):
global status
date = DateType.copy()
tempDate = DateType.copy()
entry = EntryType.copy()
dates = []
for i in range(2):
dates.append (DateType.copy())
found = False
done = False
k = 0
position = 0
inFile = ''
words = line["chars"].split()
words.pop(0)
numDates = len(words)
if numDates > 2:
status = "TOOMANYDATES"
done = True
else:
for dates1 in words:
date1 = dates1.split("-")
dates[k]["day"] = int(date1[0])
dates[k]["month"] = int(date1[1])
dates[k]["year"] = int(date1[2])
k = k + 1
if not LegalDate(dates[0]["month"], dates[0]
["day"], dates[0]["year"]):
status = "BADNUM"
found, position = Find(history, dates[0],
found, entry, position)
if status!= "BADNUM":
if not found:
dates[0]["month"] = MonthEquivalent1
(dates[0]["month"])
ReadEntry(inFile, dates[0])
dates[0]["month"] = MonthEquivalent
(dates[0]["month"])
Initialize (history)
else:
status = "ALREADYTHERE"

  1. 69
  2. 70

Chapter 8

Durk Jan de Bruin

def ExecuteDelete(line, history):
global status
date = DateType.copy()
tempDate = DateType.copy()
entry = EntryType.copy()
dates = []
for i in range(2):
dates.append (DateType.copy())
found = False
done = False
k = 0
position = 0
inFile = ''
words = line["chars"].split()
words.pop(0)
numDates = len(words)
if numDates > 2:
status = "TOOMANYDATES"
done = True
else:
for dates1 in words:
date1 = dates1.split("-")
dates[k]["day"] = int(date1[0])
dates[k]["month"] = int(date1[1])
dates[k]["year"] = int(date1[2])
k = k + 1
if not LegalDate(dates[0]["month"], dates[0]["day"], dates[0]["year"]):
status = "BADNUM"
found, position = Find(history, dates[0], found, entry, position)
if status!= "BADNUM":
if not found:
status = "NOTTHERE"
else:
a_file = open("historyFile.txt", "r")
lines = a_file.readlines()
a_file.close()
del lines[position]
new_file = open("historyFile.txt", "w+")
for line in lines:
new_file. write (line)
new_file.close()
Initialize (history)

""" Execute the given command. Its remaining arguments are still on line."""

def ExecuteCommand(line, history):
global status
if command["cmdWord"] == "LISTCMD":
ExecuteList(line, history)
if command["cmdWord"] == "ADDCMD":
ExecuteAdd(line, history)
if command["cmdWord"] == "DELETECMD":
ExecuteDelete(line, history)
if command["cmdWord"] == "HELPCMD":
ExecuteHelp(line, history)
if command["cmdWord"] == "QUITCMD":
ExecuteQuit(line, history)

# *** main program ***

Initialize(history)
done = False
while command["cmdWord"] != "QUITCMD":
GetCommand(line)
if status == "SUCCESS":
ExecuteCommand(line, history)
if status != "SUCCESS":
PrintErrorMsg(status)